aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionFreeForm
diff options
context:
space:
mode:
authorBob Zeleznik <zzzman@gmail.com>2019-05-17 21:52:49 -0400
committerBob Zeleznik <zzzman@gmail.com>2019-05-17 21:52:49 -0400
commit618b4a42795b59cde47510b86b6e25dc03e15935 (patch)
treef10a9f093df478db15e94fbf8992a32fe8ba99d0 /src/client/views/collections/collectionFreeForm
parent19fca408a19c5f7a759ff6c3bfefe27b96ec3563 (diff)
parent4e244951b7b18d7973360f423e8de80c42466228 (diff)
merged
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx58
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx130
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss24
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx87
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss107
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx404
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss26
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx367
10 files changed, 1227 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..737ffba7d
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -0,0 +1,12 @@
+.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;
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
new file mode 100644
index 000000000..63d2f7642
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -0,0 +1,58 @@
+import { observer } from "mobx-react";
+import { Utils } from "../../../../Utils";
+import "./CollectionFreeFormLinkView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { InkingControl } from "../../InkingControl";
+
+export interface CollectionFreeFormLinkViewProps {
+ A: Doc;
+ B: Doc;
+ LinkDocs: Doc[];
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+}
+
+@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();
+ }
+ }
+ 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) / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
+ return (
+ <>
+ <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
+ style={{ strokeWidth: `${l.length / 2}` }}
+ x1={`${x1}`} y1={`${y1}`}
+ x2={`${x2}`} y2={`${y2}`} />
+ <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} />
+ </>
+ );
+ }
+} \ 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..30e158603
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -0,0 +1,12 @@
+.collectionfreeformlinksview-svgCanvas{
+ transform: translate(-10000px,-10000px);
+ position: absolute;
+ top: 0;
+ left: 0;
+ 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..d5ce4e1e7
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -0,0 +1,130 @@
+import { computed, IReactionDisposer, reaction, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Utils } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DocumentView } from "../../nodes/DocumentView";
+import { CollectionViewProps } from "../CollectionSubView";
+import "./CollectionFreeFormLinksView.scss";
+import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
+import React = require("react");
+import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
+import { List } from "../../../../new_fields/List";
+import { Id } from "../../../../new_fields/RefField";
+
+@observer
+export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+
+ _brushReactionDisposer?: IReactionDisposer;
+ componentDidMount() {
+ this._brushReactionDisposer = reaction(
+ () => {
+ let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
+ },
+ () => {
+ let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ views.forEach((dstDoc, i) => {
+ views.forEach((srcDoc, j) => {
+ let dstTarg = dstDoc;
+ let srcTarg = srcDoc;
+ let x1 = NumCast(srcDoc.x);
+ let x2 = NumCast(dstDoc.x);
+ let x1w = NumCast(srcDoc.width, -1);
+ let x2w = NumCast(dstDoc.width, -1);
+ if (x1w < 0 || x2w < 0 || i === j) { }
+ else {
+ let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ });
+ let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ let found = findBrush(field);
+ if (found !== -1) {
+ console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.splice(found, 1);
+ }
+ };
+ if (Math.abs(x1 + x1w - x2) < 20) {
+ let linkDoc: Doc = new Doc();
+ linkDoc.title = "Histogram Brush";
+ linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
+
+ brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ if (findBrush(field) === -1) {
+ console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.push(linkDoc);
+ }
+ };
+ }
+ let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
+ else brushAction(dstBrushDocs);
+ if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
+ else brushAction(srcBrushDocs);
+ }
+ });
+ });
+ });
+ }
+ componentWillUnmount() {
+ if (this._brushReactionDisposer) {
+ this._brushReactionDisposer();
+ }
+ }
+ documentAnchors(view: DocumentView) {
+ let equalViews = [view];
+ let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc));
+ if (containerDoc) {
+ equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
+ }
+ if (view.props.ContainingCollectionView) {
+ let collid = view.props.ContainingCollectionView.props.Document[Id];
+ DocListCast(this.props.Document[this.props.fieldKey]).
+ filter(child =>
+ child[Id] === collid).map(view =>
+ DocumentManager.Instance.getDocumentViews(view).map(view =>
+ equalViews.push(view)));
+ }
+ 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 =>
+ drawnPairs.reduce((found, drawnPair) => {
+ let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b);
+ if (match && !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] })
+ );
+ return drawnPairs;
+ }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
+ return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
+ }
+
+ 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.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
new file mode 100644
index 000000000..c5b8fc5e8
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
@@ -0,0 +1,24 @@
+@import "globalCssVariables";
+
+.collectionFreeFormRemoteCursors-cont {
+
+ position:absolute;
+ z-index: $remoteCursors-zindex;
+ transform-origin: 'center center';
+}
+.collectionFreeFormRemoteCursors-canvas {
+
+ position:absolute;
+ width: 20px;
+ height: 20px;
+ opacity: 0.5;
+ border-radius: 50%;
+ border: 2px solid black;
+}
+.collectionFreeFormRemoteCursors-symbol {
+ font-size: 14;
+ color: black;
+ // fontStyle: "italic",
+ margin-left: -12;
+ margin-top: 4;
+} \ 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..642118d75
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -0,0 +1,87 @@
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+import { CollectionViewProps } from "../CollectionSubView";
+import "./CollectionFreeFormView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import CursorField from "../../../../new_fields/CursorField";
+import { List } from "../../../../new_fields/List";
+import { Cast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
+
+@observer
+export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
+
+ protected getCursors(): CursorField[] {
+ let doc = this.props.Document;
+
+ let id = CurrentUserUtils.id;
+ if (!id) {
+ return [];
+ }
+
+ let cursors = Cast(doc.cursors, listSpec(CursorField));
+
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id);
+ }
+
+ private crosshairs?: HTMLCanvasElement;
+ drawCrosshairs = (backgroundColor: string) => {
+ if (this.crosshairs) {
+ let ctx = this.crosshairs.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);
+ }
+ }
+ }
+
+ get sharedCursors() {
+ return this.getCursors().map(c => {
+ let m = c.data.metadata;
+ let l = c.data.position;
+ this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
+ return (
+ <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
+ style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }}
+ >
+ <canvas className="collectionFreeFormRemoteCursors-canvas"
+ ref={(el) => { if (el) this.crosshairs = el; }}
+ width={20}
+ height={20}
+ />
+ <p className="collectionFreeFormRemoteCursors-symbol">
+ {m.identifier[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..063c9e2cf
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -0,0 +1,107 @@
+@import "../../globalCssVariables";
+
+.collectionfreeformview-ease {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+ transition: transform 1s;
+}
+
+.collectionfreeformview-none {
+ position: inherit;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+}
+
+.collectionfreeformview-container {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ 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: absolute;
+ .marqueeView {
+ overflow: hidden;
+ }
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+
+.collectionfreeformview-overlay {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ }
+
+ .formattedTextBox-cont {
+ background: $light-color-secondary;
+ overflow: visible;
+ }
+
+ opacity: 0.99;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position:absolute;
+ .marqueeView {
+ 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;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
new file mode 100644
index 000000000..ad32f70dc
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -0,0 +1,404 @@
+import { action, computed, trace } from "mobx";
+import { observer } from "mobx-react";
+import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DragManager } from "../../../util/DragManager";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch } from "../../../util/UndoManager";
+import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { CollectionSubView } from "../CollectionSubView";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
+import "./CollectionFreeFormView.scss";
+import { MarqueeView } from "./MarqueeView";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types";
+import { pageSchema } from "../../nodes/ImageBox";
+import { Id } from "../../../../new_fields/RefField";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { HistoryUtil } from "../../../util/History";
+
+export const panZoomSchema = createSchema({
+ panX: "number",
+ panY: "number",
+ scale: "number"
+});
+
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
+
+@observer
+export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
+ public static RIGHT_BTN_DRAG = false;
+ private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
+ private _lastX: number = 0;
+ private _lastY: number = 0;
+ private get _pwidth() { return this.props.PanelWidth(); }
+ private get _pheight() { return this.props.PanelHeight(); }
+
+ @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
+ private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+ private panX = () => this.Document.panX || 0;
+ private panY = () => this.Document.panY || 0;
+ private zoomScaling = () => this.Document.scale || 1;
+ private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections
+ private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections
+ private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
+ private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
+ private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
+ private addLiveTextBox = (newBox: Doc) => {
+ this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ this.addDocument(newBox, false);
+ }
+ private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
+ this.props.addDocument(newBox, false);
+ this.bringToFront(newBox);
+ return true;
+ }
+ private selectDocuments = (docs: Doc[]) => {
+ SelectionManager.DeselectAll;
+ docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
+ SelectionManager.SelectDoc(dv!, true));
+ }
+ public getActiveDocuments = () => {
+ const curPage = FieldValue(this.Document.curPage, -1);
+ return this.children.filter(doc => {
+ var page = NumCast(doc.page, -1);
+ return page === curPage || page === -1;
+ });
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.droppedDocuments.length) {
+ let dragDoc = de.data.droppedDocuments[0];
+ let zoom = NumCast(dragDoc.zoomBasis, 1);
+ let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+ let x = xp - de.data.xOffset / zoom;
+ let y = yp - de.data.yOffset / zoom;
+ let dropX = NumCast(de.data.droppedDocuments[0].x);
+ let dropY = NumCast(de.data.droppedDocuments[0].y);
+ de.data.droppedDocuments.map(d => {
+ d.x = x + NumCast(d.x) - dropX;
+ d.y = y + NumCast(d.y) - dropY;
+ if (!NumCast(d.width)) {
+ d.width = 300;
+ }
+ if (!NumCast(d.height)) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+ }
+ this.bringToFront(d);
+ });
+ SelectionManager.ReselectAll();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
+ (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
+ (e.button === 0 && e.altKey)) && this.props.active())) ||
+ (!CollectionFreeFormView.RIGHT_BTN_DRAG &&
+ ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ }
+ }
+
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ if (!e.cancelBubble) {
+ let x = this.Document.panX || 0;
+ let y = this.Document.panY || 0;
+ let docs = this.children || [];
+ let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+ if (!this.isAnnotationOverlay) {
+ let minx = docs.length ? NumCast(docs[0].x) : 0;
+ let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+ let miny = docs.length ? NumCast(docs[0].y) : 0;
+ let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+ let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ let x = NumCast(doc.x);
+ let xe = x + NumCast(doc.width);
+ let y = NumCast(doc.y);
+ let ye = y + NumCast(doc.height);
+ return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
+ [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
+ }, [[minx, maxx], [miny, maxy]]);
+ let ink = Cast(this.props.Document.ink, InkField);
+ if (ink && ink.inkData) {
+ ink.inkData.forEach((value: StrokeData, key: string) => {
+ let bounds = InkingCanvas.StrokeRect(value);
+ ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
+ ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
+ });
+ }
+
+ let panelwidth = this._pwidth / this.zoomScaling() / 2;
+ let panelheight = this._pheight / this.zoomScaling() / 2;
+ if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx;
+ if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx;
+ if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy;
+ if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy;
+ }
+ this.setPan(x - dx, y - dy);
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerWheel = (e: React.WheelEvent): void => {
+ // if (!this.props.active()) {
+ // return;
+ // }
+ let childSelected = this.children.some(doc => {
+ var dv = DocumentManager.Instance.getDocumentView(doc);
+ return dv && SelectionManager.IsSelected(dv) ? true : false;
+ });
+ if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
+ return;
+ }
+ e.stopPropagation();
+ const coefficient = 1000;
+
+ if (e.ctrlKey) {
+ let deltaScale = (1 - (e.deltaY / coefficient));
+ let nw = this.nativeWidth * deltaScale;
+ let nh = this.nativeHeight * deltaScale;
+ if (nw && nh) {
+ this.props.Document.nativeWidth = nw;
+ this.props.Document.nativeHeight = nh;
+ }
+ 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 deltaScale = (1 - (e.deltaY / coefficient));
+ if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
+ deltaScale = 1 / this.zoomScaling();
+ }
+ if (deltaScale < 0) deltaScale = -deltaScale;
+ let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY);
+ let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
+
+ let safeScale = Math.abs(localTransform.Scale);
+ this.props.Document.scale = Math.abs(safeScale);
+ this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ setPan(panX: number, panY: number) {
+ this.panDisposer && clearTimeout(this.panDisposer);
+ this.props.Document.panTransformType = "None";
+ var scale = this.getLocalTransform().inverse().Scale;
+ const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
+ const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
+ this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ }
+
+ @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 => {
+ }
+
+ bringToFront = (doc: Doc) => {
+ const docs = this.children;
+ docs.slice().sort((doc1, doc2) => {
+ if (doc1 === doc) return 1;
+ if (doc2 === doc) return -1;
+ return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
+ }).forEach((doc, index) => doc.zIndex = index + 1);
+ doc.zIndex = docs.length + 1;
+ }
+
+ panDisposer?: NodeJS.Timeout;
+ focusDocument = (doc: Doc) => {
+ const panX = this.Document.panX;
+ const panY = this.Document.panY;
+ const id = this.Document[Id];
+ const state = HistoryUtil.getState();
+ // TODO This technically isn't correct if type !== "doc", as
+ // currently nothing is done, but we should probably push a new state
+ if (state.type === "doc" && panX !== undefined && panY !== undefined) {
+ const init = state.initializers[id];
+ if (!init) {
+ state.initializers[id] = {
+ panX, panY
+ };
+ HistoryUtil.pushState(state);
+ } else if (init.panX !== panX || init.panY !== panY) {
+ init.panX = panX;
+ init.panY = panY;
+ HistoryUtil.pushState(state);
+ }
+ }
+ SelectionManager.DeselectAll();
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
+ const newState = HistoryUtil.getState();
+ newState.initializers[id] = { panX: newPanX, panY: newPanY };
+ HistoryUtil.pushState(newState);
+ this.setPan(newPanX, newPanY);
+ this.props.Document.panTransformType = "Ease";
+ this.props.focus(this.props.Document);
+ this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false
+ }
+
+
+ getDocumentViewProps(document: Doc): DocumentViewProps {
+ return {
+ Document: document,
+ toggleMinimized: emptyFunction,
+ addDocument: this.props.addDocument,
+ removeDocument: this.props.removeDocument,
+ moveDocument: this.props.moveDocument,
+ ScreenToLocalTransform: this.getTransform,
+ isTopMost: false,
+ selectOnLoad: document[Id] === this._selectOnLoaded,
+ PanelWidth: document[WidthSym],
+ PanelHeight: document[HeightSym],
+ ContentScaling: returnOne,
+ ContainingCollectionView: this.props.CollectionView,
+ focus: this.focusDocument,
+ parentActive: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
+ bringToFront: this.bringToFront,
+ addDocTab: this.props.addDocTab,
+ };
+ }
+
+ @computed.struct
+ get views() {
+ let curPage = FieldValue(this.Document.curPage, -1);
+ let docviews = this.children.reduce((prev, doc) => {
+ if (!(doc instanceof Doc)) return prev;
+ var page = NumCast(doc.page, -1);
+ if (page === curPage || page === -1) {
+ let minim = BoolCast(doc.isMinimized, false);
+ if (minim === undefined || !minim) {
+ prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
+ }
+ }
+ return prev;
+ }, [] as JSX.Element[]);
+
+ setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
+
+ return docviews;
+ }
+
+ @action
+ onCursorMove = (e: React.PointerEvent) => {
+ super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
+ }
+
+ private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />];
+ render() {
+ const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
+ const easing = () => this.props.Document.panTransformType === "Ease";
+ return (
+ <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel}
+ style={{ borderRadius: "inherit" }}
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} >
+ <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
+ addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+ <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
+ easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+
+ <CollectionFreeFormLinksView {...this.props} key="freeformLinks">
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+ {this.childViews}
+ </InkingCanvas>
+ </CollectionFreeFormLinksView>
+ <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
+ </CollectionFreeFormViewPannableContents>
+ </MarqueeView>
+ <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+ </div>
+ );
+ }
+}
+
+@observer
+class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
+ @computed get overlayView() {
+ return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ }
+ render() {
+ return this.overlayView;
+ }
+}
+
+@observer
+class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
+ @computed get backgroundView() {
+ return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ }
+ render() {
+ return this.props.Document.backgroundLayout ? this.backgroundView : (null);
+ }
+}
+
+interface CollectionFreeFormViewPannableContentsProps {
+ centeringShiftX: () => number;
+ centeringShiftY: () => number;
+ panX: () => number;
+ panY: () => number;
+ zoomScaling: () => number;
+ easing: () => boolean;
+}
+
+@observer
+class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{
+ render() {
+ let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none");
+ const cenx = this.props.centeringShiftX();
+ const ceny = this.props.centeringShiftY();
+ const panx = -this.props.panX();
+ const pany = -this.props.panY();
+ const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire
+ return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}>
+ {this.props.children}
+ </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..6e8ec8662
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -0,0 +1,26 @@
+
+.marqueeView {
+ position: inherit;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.marquee {
+ border-style: dashed;
+ box-sizing: border-box;
+ position: absolute;
+ border-width: 1px;
+ border-color: black;
+ pointer-events: none;
+ .marquee-legend {
+ bottom:-18px;
+ left:0;
+ position: absolute;
+ font-size: 9;
+ white-space:nowrap;
+ }
+ .marquee-legend::after {
+ content: "Press: c (collection), s (summary), r (replace) or Delete"
+ }
+} \ 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..c3c4115b8
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -0,0 +1,367 @@
+import * as htmlToImage from "html-to-image";
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Docs } from "../../../documents/Documents";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { PreviewCursor } from "../../PreviewCursor";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./MarqueeView.scss";
+import React = require("react");
+import { Utils } from "../../../../Utils";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, Cast } from "../../../../new_fields/Types";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { List } from "../../../../new_fields/List";
+import { ImageField } from "../../../../new_fields/URLField";
+import { Template, Templates } from "../../Templates";
+import { Gateway } from "../../../northstar/manager/Gateway";
+import { DocServer } from "../../../DocServer";
+import { Id } from "../../../../new_fields/RefField";
+
+interface MarqueeViewProps {
+ getContainerTransform: () => Transform;
+ getTransform: () => Transform;
+ container: CollectionFreeFormView;
+ addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ activeDocuments: () => Doc[];
+ selectDocuments: (docs: Doc[]) => void;
+ removeDocument: (doc: Doc) => boolean;
+ addLiveTextDocument: (doc: Doc) => void;
+ isSelected: () => boolean;
+}
+
+@observer
+export class MarqueeView extends React.Component<MarqueeViewProps>
+{
+ private _mainCont = React.createRef<HTMLDivElement>();
+ @observable _lastX: number = 0;
+ @observable _lastY: number = 0;
+ @observable _downX: number = 0;
+ @observable _downY: number = 0;
+ @observable _visible: boolean = false;
+ _commandExecuted = false;
+
+ @action
+ cleanupInteractions = (all: boolean = false) => {
+ if (all) {
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ }
+ document.removeEventListener("keydown", this.marqueeCommand, true);
+ this._visible = false;
+ }
+
+ @undoBatch
+ @action
+ onKeyPress = (e: KeyboardEvent) => {
+ //make textbox and add it to this collection
+ let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
+ if (e.key === "q" && e.ctrlKey) {
+ e.preventDefault();
+ (async () => {
+ let text: string = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ for (let i = 0; i < ns.length - 1; i++) {
+ while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
+ ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
+ ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
+ ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
+ let sub = ns[i].endsWith("\r") ? 1 : 0;
+ let br = ns[i + 1].trim() === "";
+ ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
+ if (br) break;
+ }
+ }
+ ns.map(line => {
+ let indent = line.search(/\S|$/);
+ let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ this.props.addDocument(newBox, false);
+ y += 40 * this.props.getTransform().Scale;
+ });
+ })();
+ } else if (e.key === "b" && e.ctrlKey) {
+ //heuristically converts pasted text into a table.
+ // assumes each entry is separated by a tab
+ // skips all rows until it gets to a row with more than one entry
+ // assumes that 1st row has header entry for each column
+ // assumes subsequent rows have entries for each column header OR
+ // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
+ // assumes each cell is a string or a number
+ e.preventDefault();
+ (async () => {
+ let text: string = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ while (ns.length > 0 && ns[0].split("\t").length < 2) {
+ ns.splice(0, 1);
+ }
+ if (ns.length > 0) {
+ let columns = ns[0].split("\t");
+ let docList: Doc[] = [];
+ let groupAttr: string | number = "";
+ for (let i = 1; i < ns.length - 1; i++) {
+ let values = ns[i].split("\t");
+ if (values.length === 1 && columns.length > 1) {
+ groupAttr = values[0];
+ continue;
+ }
+ let doc = new Doc();
+ columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
+ if (groupAttr) {
+ doc._group = groupAttr;
+ }
+ doc.title = i.toString();
+ docList.push(doc);
+ }
+ let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+
+ this.props.addDocument(newCol, false);
+ }
+ })();
+ } else {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ this.props.addLiveTextDocument(newBox);
+ }
+ e.stopPropagation();
+ }
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = this._lastX = e.pageX;
+ this._downY = this._lastY = e.pageY;
+ this._commandExecuted = false;
+ PreviewCursor.Visible = false;
+ if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) ||
+ (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) {
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("keydown", this.marqueeCommand, true);
+ // bcz: do we need this? it kills the context menu on the main collection
+ // e.stopPropagation();
+ }
+ if (e.altKey) {
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ if (!e.cancelBubble) {
+ if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD ||
+ Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) {
+ if (!this._commandExecuted) {
+ this._visible = true;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ if (e.altKey) {
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ if (this._visible) {
+ 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]);
+ }
+ this.cleanupInteractions(true);
+ if (e.altKey) {
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onClick = (e: React.MouseEvent): void => {
+ if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress);
+ // let the DocumentView stopPropagation of this event when it selects this document
+ } else { // why do we get a click event when the cursor have moved a big distance?
+ // let's cut it off here so no one else has to deal with it.
+ e.stopPropagation();
+ }
+ }
+
+ 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]) };
+ }
+
+ @undoBatch
+ @action
+ marqueeCommand = async (e: KeyboardEvent) => {
+ if (this._commandExecuted || (e as any).propagationIsStopped) {
+ return;
+ }
+ if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") {
+ this._commandExecuted = true;
+ e.stopPropagation();
+ (e as any).propagationIsStopped = true;
+ this.marqueeSelect().map(d => this.props.removeDocument(d));
+ let ink = Cast(this.props.container.props.Document.ink, InkField);
+ if (ink) {
+ this.marqueeInkDelete(ink.inkData);
+ }
+ SelectionManager.DeselectAll();
+ this.cleanupInteractions(false);
+ e.stopPropagation();
+ }
+ if (e.key === "c" || e.key === "s" || e.key === "e" || e.key === "p") {
+ this._commandExecuted = true;
+ e.stopPropagation();
+ (e as any).propagationIsStopped = true;
+ let bounds = this.Bounds;
+ let selected = this.marqueeSelect();
+ if (e.key === "c") {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ }
+ let ink = Cast(this.props.container.props.Document.ink, InkField);
+ let inkData = ink ? ink.inkData : undefined;
+ let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
+ let newCollection = Docs.FreeformDocument(selected, {
+ x: bounds.left,
+ y: bounds.top,
+ panX: 0,
+ panY: 0,
+ borderRounding: e.key === "e" ? -1 : undefined,
+ scale: zoomBasis,
+ width: bounds.width * zoomBasis,
+ height: bounds.height * zoomBasis,
+ ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
+ title: e.key === "s" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
+ });
+ this.marqueeInkDelete(inkData);
+
+ if (e.key === "s" || e.key === "p") {
+
+ htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 1 }).then((dataUrl) => {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+ summary.proto!.thumbnail = new ImageField(new URL(dataUrl));
+ summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]);
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ this.props.addDocument(newCollection, false);
+ summary.proto!.summarizedDocs = new List<Doc>(selected.map(s => s.proto!));
+ //summary.proto!.maximizeOnRight = true;
+ //summary.proto!.isButton = true;
+ //let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
+ // selected.map(summarizedDoc => {
+ // let maxx = NumCast(summarizedDoc.x, undefined);
+ // let maxy = NumCast(summarizedDoc.y, undefined);
+ // let maxw = NumCast(summarizedDoc.width, undefined);
+ // let maxh = NumCast(summarizedDoc.height, undefined);
+ // summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]);
+ // });
+ this.props.addLiveTextDocument(summary);
+ });
+ }
+ else {
+ this.props.addDocument(newCollection, false);
+ SelectionManager.DeselectAll();
+ this.props.selectDocuments([newCollection]);
+ }
+ this.cleanupInteractions(false);
+ }
+ }
+ @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 => ({ 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));
+ Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata));
+ }
+ }
+
+ marqueeSelect() {
+ let selRect = this.Bounds;
+ let selection: Doc[] = [];
+ this.props.activeDocuments().map(doc => {
+ var z = NumCast(doc.zoomBasis, 1);
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width) / z;
+ var h = NumCast(doc.height) / z;
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
+ selection.push(doc);
+ }
+ });
+ return selection;
+ }
+
+ @computed
+ get marqueeDiv() {
+ let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
+ <span className="marquee-legend" />
+ </div>;
+ }
+
+ render() {
+ let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+ return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
+ {!this._visible ? null : this.marqueeDiv}
+ <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
+ {this.props.children}
+ </div>
+ </div>
+ </div>;
+ }
+} \ No newline at end of file