aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionFreeForm
diff options
context:
space:
mode:
authorStanley Yip <stanley_yip@brown.edu>2020-02-09 14:58:57 -0500
committerStanley Yip <stanley_yip@brown.edu>2020-02-09 14:58:57 -0500
commitf6179334d6f2942631caa17b7c8ae2531d87c7c4 (patch)
tree091da0ef7bedb900c958c28cebe4058fade644cf /src/client/views/collections/collectionFreeForm
parent07141291bee793955d7061f4e479942d7aceda67 (diff)
parent87167fd126e161b29d8d798a5f04e3cf159aae16 (diff)
recommender system works
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx65
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx101
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx22
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss33
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx936
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx57
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss4
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx465
9 files changed, 1183 insertions, 512 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 48d330674..be1317b25 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,11 +1,15 @@
import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
-import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
+import { NumCast, StrCast, Cast, DateCast } from "../../../../new_fields/Types";
import { ScriptBox } from "../../ScriptBox";
import { CompileScript } from "../../../util/Scripting";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { OverlayView, OverlayElementOptions } from "../../OverlayView";
import { emptyFunction } from "../../../../Utils";
import React = require("react");
+import { ObservableMap, runInAction } from "mobx";
+import { Id, ToString } from "../../../../new_fields/FieldSymbols";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { RefField } from "../../../../new_fields/RefField";
interface PivotData {
type: string;
@@ -31,13 +35,20 @@ export interface ViewDefResult {
bounds?: ViewDefBounds;
}
-export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
- let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
+function toLabel(target: FieldResult<Field>) {
+ if (target instanceof ObjectField || target instanceof RefField) {
+ return target[ToString]();
+ }
+ return String(target);
+}
+
+export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
+ const pivotFieldKey = toLabel(pivotDoc.pivotField);
for (const doc of childDocs) {
- const val = doc[StrCast(pivotDoc.pivotField, "title")];
+ const val = Field.toString(doc[pivotFieldKey] as Field);
if (val) {
!pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
pivotColumnGroups.get(val)!.push(doc);
@@ -45,60 +56,72 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
}
const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity);
- const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
+ let numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
const docMap = new Map<Doc, ViewDefBounds>();
const groupNames: PivotData[] = [];
+ numCols = Math.min(panelDim[0] / pivotAxisWidth, numCols);
+ const expander = 1.05;
+ const gap = .15;
let x = 0;
pivotColumnGroups.forEach((val, key) => {
let y = 0;
let xCount = 0;
groupNames.push({
type: "text",
- text: String(key),
+ text: toLabel(key),
x,
y: pivotAxisWidth + 50,
- width: pivotAxisWidth * 1.25 * numCols,
- height: 100,
+ width: pivotAxisWidth * expander * numCols,
+ height: NumCast(pivotDoc.pivotFontSize, 10),
fontSize: NumCast(pivotDoc.pivotFontSize, 10)
});
for (const doc of val) {
- let layoutDoc = Doc.Layout(doc);
+ const layoutDoc = Doc.Layout(doc);
+ let wid = pivotAxisWidth;
+ let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
+ if (hgt > pivotAxisWidth) {
+ hgt = pivotAxisWidth;
+ wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
+ }
docMap.set(doc, {
- x: x + xCount * pivotAxisWidth * 1.25,
+ x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0),
y: -y,
- width: pivotAxisWidth,
- height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+ width: wid,
+ height: hgt
});
xCount++;
if (xCount >= numCols) {
xCount = 0;
- y += pivotAxisWidth * 1.25;
+ y += pivotAxisWidth * expander;
}
}
- x += pivotAxisWidth * 1.25 * (numCols + 1);
+ x += pivotAxisWidth * (numCols * expander + gap);
});
childPairs.map(pair => {
- let defaultPosition = {
+ const defaultPosition = {
x: NumCast(pair.layout.x),
y: NumCast(pair.layout.y),
z: NumCast(pair.layout.z),
- width: NumCast(pair.layout.width),
- height: NumCast(pair.layout.height)
+ width: NumCast(pair.layout._width),
+ height: NumCast(pair.layout._height)
};
const pos = docMap.get(pair.layout) || defaultPosition;
- layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
+ const data = poolData.get(pair.layout[Id]);
+ if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height) {
+ runInAction(() => poolData.set(pair.layout[Id], { transition: "transform 1s", ...pos }));
+ }
});
- return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) };
+ return { elements: viewDefsToJSX(groupNames) };
}
export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
return () => {
- let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
+ const addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox
const scriptField = Cast(doc[key], ScriptField);
- let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
+ const scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
// tslint:disable-next-line: no-unnecessary-callback-wrapper
onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below
onSave={(text, onError) => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 837413842..b8fbaef5c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -6,7 +6,9 @@ import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
import { DocumentType } from "../../../documents/DocumentTypes";
-import { observable, action } from "mobx";
+import { observable, action, reaction, IReactionDisposer } from "mobx";
+import { StrCast } from "../../../../new_fields/Types";
+import { Id } from "../../../../new_fields/FieldSymbols";
export interface CollectionFreeFormLinkViewProps {
A: DocumentView;
@@ -16,36 +18,87 @@ export interface CollectionFreeFormLinkViewProps {
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
- @observable _alive: number = 0;
- @observable _opacity: number = 1;
+ @observable _opacity: number = 0;
+ _anchorDisposer: IReactionDisposer | undefined;
@action
componentDidMount() {
- this._alive = 1;
- setTimeout(this.rerender, 50);
- setTimeout(action(() => this._opacity = 0.05), 50);
+ this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)],
+ action(() => {
+ setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
+ setTimeout(action(() => this._opacity = 0.05), 750); // this will unhighlight the link line.
+ const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!);
+ const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!);
+ const a = adiv.getBoundingClientRect();
+ const b = bdiv.getBoundingClientRect();
+ const abounds = adiv.parentElement!.getBoundingClientRect();
+ const bbounds = bdiv.parentElement!.getBoundingClientRect();
+ const apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height,
+ bbounds.left, bbounds.top, bbounds.width, bbounds.height,
+ a.left + a.width / 2, a.top + a.height / 2);
+ const bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height,
+ abounds.left, abounds.top, abounds.width, abounds.height,
+ apt.point.x, apt.point.y);
+ const afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
+ const bfield = afield === "anchor1" ? "anchor2" : "anchor1";
+
+ // really hacky stuff to make the DocuLinkBox display where we want it to:
+ // if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
+ // otherwise, we just use the computed nearest point on the document boundary to the target Document
+ const targetAhyperlink = window.document.getElementById((this.props.LinkDocs[0][afield] as Doc)[Id]);
+ const targetBhyperlink = window.document.getElementById((this.props.LinkDocs[0][bfield] as Doc)[Id]);
+ if (!targetBhyperlink) {
+ this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
+ this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
+ } else {
+ setTimeout(() => {
+ (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc);
+ const m = targetBhyperlink.getBoundingClientRect();
+ const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
+ this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100;
+ this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100;
+ }, 0);
+ }
+ if (!targetAhyperlink) {
+ this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100;
+ this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100;
+ } else {
+ setTimeout(() => {
+ (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc);
+ const m = targetAhyperlink.getBoundingClientRect();
+ const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
+ this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100;
+ this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100;
+ }, 0);
+ }
+ })
+ , { fireImmediately: true });
}
@action
componentWillUnmount() {
- this._alive = 0;
+ this._anchorDisposer?.();
}
- rerender = action(() => {
- if (this._alive) {
- setTimeout(this.rerender, 50);
- this._alive++;
- }
- });
render() {
- let y = this._alive;
- let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
- let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
- let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
- let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
- let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2);
- let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
- return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
- style={{ opacity: this._opacity }}
- x1={`${pt1[0]}`} y1={`${pt1[1]}`}
- x2={`${pt2[0]}`} y2={`${pt2[1]}`} />);
+ const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
+ const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
+ const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height,
+ b.left, b.top, b.width, b.height,
+ a.left + a.width / 2, a.top + a.height / 2);
+ const bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height,
+ a.left, a.top, a.width, a.height,
+ apt.point.x, apt.point.y);
+ const pt1 = [apt.point.x, apt.point.y];
+ const pt2 = [bpt.point.x, bpt.point.y];
+ const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
+ const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
+ return !aActive && !bActive ? (null) :
+ <line key="linkLine" className="collectionfreeformlinkview-linkLine"
+ style={{ opacity: this._opacity, strokeDasharray: "2 2" }}
+ x1={`${pt1[0]}`} y1={`${pt1[1]}`}
+ x2={`${pt2[0]}`} y2={`${pt2[1]}`} />;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index e9191c176..044d35eca 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -72,11 +72,11 @@ export class CollectionFreeFormLinksView extends React.Component {
}
@computed
get uniqueConnections() {
- let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+ const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
if (!drawnPairs.reduce((found, drawnPair) => {
- let match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b);
- let match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a);
- let match = match1 || match2;
+ const match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b);
+ const match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a);
+ const match = match1 || match2;
if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
drawnPair.l.push(connection.l);
}
@@ -91,13 +91,11 @@ export class CollectionFreeFormLinksView extends React.Component {
}
render() {
- return (
- <div className="collectionfreeformlinksview-container">
- <svg className="collectionfreeformlinksview-svgCanvas">
- {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections}
- </svg>
- {this.props.children}
- </div>
- );
+ return <div className="collectionfreeformlinksview-container">
+ <svg className="collectionfreeformlinksview-svgCanvas">
+ {SelectionManager.GetIsDragging() ? (null) : 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
index b8148852d..bb9ae4326 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -13,14 +13,14 @@ import v5 = require("uuid/v5");
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
protected getCursors(): CursorField[] {
- let doc = this.props.Document;
+ const doc = this.props.Document;
- let id = CurrentUserUtils.id;
+ const id = CurrentUserUtils.id;
if (!id) {
return [];
}
- let cursors = Cast(doc.cursors, listSpec(CursorField));
+ const cursors = Cast(doc.cursors, listSpec(CursorField));
const now = mobxUtils.now();
// const now = Date.now();
@@ -30,7 +30,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
private crosshairs?: HTMLCanvasElement;
drawCrosshairs = (backgroundColor: string) => {
if (this.crosshairs) {
- let ctx = this.crosshairs.getContext('2d');
+ const ctx = this.crosshairs.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, 20, 20);
@@ -62,8 +62,8 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
get sharedCursors() {
return this.getCursors().map(c => {
- let m = c.data.metadata;
- let l = c.data.position;
+ const m = c.data.metadata;
+ const l = c.data.position;
this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
return (
<div key={m.id} className="collectionFreeFormRemoteCursors-cont"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index bb1a12f88..58fb81453 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,5 +1,6 @@
@import "../../globalCssVariables";
+.collectionfreeformview-none,
.collectionfreeformview-ease {
position: inherit;
top: 0;
@@ -7,16 +8,15 @@
width: 100%;
height: 100%;
transform-origin: left top;
+ border-radius: inherit;
+}
+
+.collectionfreeformview-ease {
transition: transform 1s;
}
.collectionfreeformview-none {
- position: inherit;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- transform-origin: left top;
+ touch-action: none;
}
.collectionFreeform-customText {
@@ -25,6 +25,24 @@
}
.collectionfreeformview-container {
+ // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions.
+ touch-action: none;
+
+ .collectionfreeformview-placeholder {
+ background: gray;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ .collectionfreeformview-placeholderSpan {
+ font-size: 32;
+ display: flex;
+ text-align: center;
+ margin: auto;
+ background: #80808069;
+ }
+ }
+
.collectionfreeformview>.jsx-parser {
position: inherit;
height: 100%;
@@ -41,7 +59,6 @@
// linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
// background-size: 30px 30px;
// }
- opacity: 0.99;
border: 0px solid $light-color-secondary;
border-radius: inherit;
box-sizing: border-box;
@@ -50,6 +67,8 @@
left: 0;
width: 100%;
height: 100%;
+ align-items: center;
+ display: flex;
}
// selection border...?
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bfec545c6..53fe2b18c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,54 +1,63 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc";
+import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast, FieldValue } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
-import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import { Docs } from "../../../documents/Documents";
+import { DocServer } from "../../../DocServer";
+import { Docs, DocUtils } from "../../../documents/Documents";
import { DocumentType } from "../../../documents/DocumentTypes";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
+import { InteractionUtils } from "../../../util/InteractionUtils";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
-import { InkingCanvas } from "../../InkingCanvas";
+import { InkingControl } from "../../InkingControl";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
+import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
+import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import { DocServer } from "../../../DocServer";
-import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
+import { computedFn } from "mobx-utils";
+import { TraceMobx } from "../../../../new_fields/util";
+import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { RichTextField } from "../../../../new_fields/RichTextField";
+import { List } from "../../../../new_fields/List";
import { DocumentViewProps } from "../../nodes/DocumentView";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
export const panZoomSchema = createSchema({
- panX: "number",
- panY: "number",
+ _panX: "number",
+ _panY: "number",
scale: "number",
arrangeScript: ScriptField,
arrangeInit: ScriptField,
useClusters: "boolean",
- isRuleProvider: "boolean",
fitToBox: "boolean",
+ xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
panTransformType: "string",
scrollHeight: "number",
fitX: "number",
@@ -64,20 +73,28 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _lastX: number = 0;
private _lastY: number = 0;
+ private _inkToTextStartX: number | undefined;
+ private _inkToTextStartY: number | undefined;
+ private _wordPalette: Map<string, string> = new Map<string, string>();
private _clusterDistance: number = 75;
private _hitCluster = false;
+ private _layoutComputeReaction: IReactionDisposer | undefined;
+ private _layoutPoolData = new ObservableMap<string, any>();
+
+ public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive
+ @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
@observable _clusterSets: (Doc[])[] = [];
- @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
+ @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
@computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
- @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
- @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
+ @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); }
+ @computed get nativeWidth() { return this.Document._fitToContent ? 0 : NumCast(this.Document._nativeWidth); }
+ @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight); }
private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
private easing = () => this.props.Document.panTransformType === "Ease";
- private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0;
- private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0;
+ private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0;
+ private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0;
private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ?
Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
this.Document.scale || 1)
@@ -89,18 +106,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
- let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
- let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0;
- if (heading === 0) {
- let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 :
- DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0);
- heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
- }
- !this.Document.isRuleProvider && (newBox.heading = heading);
this.addDocument(newBox);
}
private addDocument = (newBox: Doc) => {
- let added = this.props.addDocument(newBox);
+ const added = this.props.addDocument(newBox);
added && this.bringToFront(newBox);
added && this.updateCluster(newBox);
return added;
@@ -117,54 +126,54 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onDrop = (e: React.DragEvent): Promise<void> => {
- var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ const pt = this.getTransform().transformPoint(e.pageX, e.pageY);
return super.onDrop(e, { x: pt[0], y: pt[1] });
}
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- let xf = this.getTransform();
- let xfo = this.getTransformOverlay();
- let [xp, yp] = xf.transformPoint(de.x, de.y);
- let [xpo, ypo] = xfo.transformPoint(de.x, de.y);
+ const xf = this.getTransform();
+ const xfo = this.getTransformOverlay();
+ const [xp, yp] = xf.transformPoint(de.x, de.y);
+ const [xpo, ypo] = xfo.transformPoint(de.x, de.y);
if (super.drop(e, de)) {
- if (de.data instanceof DragManager.DocumentDragData) {
- if (de.data.droppedDocuments.length) {
- let firstDoc = de.data.droppedDocuments[0];
- let z = NumCast(firstDoc.z);
- let x = (z ? xpo : xp) - de.data.offset[0];
- let y = (z ? ypo : yp) - de.data.offset[1];
- let dropX = NumCast(firstDoc.x);
- let dropY = NumCast(firstDoc.y);
- de.data.droppedDocuments.forEach(action((d: Doc) => {
- let layoutDoc = Doc.Layout(d);
+ if (de.complete.docDragData) {
+ if (de.complete.docDragData.droppedDocuments.length) {
+ const firstDoc = de.complete.docDragData.droppedDocuments[0];
+ const z = NumCast(firstDoc.z);
+ const x = (z ? xpo : xp) - de.complete.docDragData.offset[0];
+ const y = (z ? ypo : yp) - de.complete.docDragData.offset[1];
+ const dropX = NumCast(firstDoc.x);
+ const dropY = NumCast(firstDoc.y);
+ de.complete.docDragData.droppedDocuments.forEach(action((d: Doc) => {
+ const layoutDoc = Doc.Layout(d);
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
- if (!NumCast(layoutDoc.width)) {
- layoutDoc.width = 300;
+ if (!NumCast(layoutDoc._width)) {
+ layoutDoc._width = 300;
}
- if (!NumCast(layoutDoc.height)) {
- let nw = NumCast(layoutDoc.nativeWidth);
- let nh = NumCast(layoutDoc.nativeHeight);
- layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
+ if (!NumCast(layoutDoc._height)) {
+ const nw = NumCast(layoutDoc._nativeWidth);
+ const nh = NumCast(layoutDoc._nativeHeight);
+ layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300;
}
this.bringToFront(d);
}));
- de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]);
+ (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments);
}
}
- else if (de.data instanceof DragManager.AnnotationDragData) {
- if (de.data.dropDocument) {
- let dragDoc = de.data.dropDocument;
- let x = xp - de.data.offset[0];
- let y = yp - de.data.offset[1];
- let dropX = NumCast(dragDoc.x);
- let dropY = NumCast(dragDoc.y);
+ else if (de.complete.annoDragData) {
+ if (de.complete.annoDragData.dropDocument) {
+ const dragDoc = de.complete.annoDragData.dropDocument;
+ const x = xp - de.complete.annoDragData.offset[0];
+ const y = yp - de.complete.annoDragData.offset[1];
+ const dropX = NumCast(dragDoc.x);
+ const dropY = NumCast(dragDoc.y);
dragDoc.x = x + NumCast(dragDoc.x) - dropX;
dragDoc.y = y + NumCast(dragDoc.y) - dropY;
- de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
+ de.complete.annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
this.bringToFront(dragDoc);
}
}
@@ -174,30 +183,30 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
pickCluster(probe: number[]) {
return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
- let layoutDoc = Doc.Layout(cd);
- let cx = NumCast(cd.x) - this._clusterDistance;
- let cy = NumCast(cd.y) - this._clusterDistance;
- let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
- let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+ const layoutDoc = Doc.Layout(cd);
+ const cx = NumCast(cd.x) - this._clusterDistance;
+ const cy = NumCast(cd.y) - this._clusterDistance;
+ const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance;
+ const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance;
return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
NumCast(cd.cluster) : cluster;
}, -1);
}
- tryDragCluster(e: PointerEvent) {
- let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
- if (cluster !== -1) {
- let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
- let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
- let de = new DragManager.DocumentDragData(eles);
- de.moveDocument = this.props.moveDocument;
- const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
- de.offset = this.getTransform().transformDirection(e.x - left, e.y - top);
- de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
- DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
- handlers: { dragComplete: action(emptyFunction) },
- hideSource: !de.dropAction
- });
- return true;
+ tryDragCluster(e: PointerEvent | TouchEvent) {
+ const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0);
+ if (ptsParent) {
+ const cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY));
+ if (cluster !== -1) {
+ const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
+ const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
+ const de = new DragManager.DocumentDragData(eles);
+ de.moveDocument = this.props.moveDocument;
+ const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
+ de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top);
+ de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+ DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { hideSource: !de.dropAction });
+ return true;
+ }
}
return false;
@@ -210,13 +219,48 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
}
+ @action
+ updateClusterDocs(docs: Doc[]) {
+ const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
+ if (this.props.Document.useClusters) {
+ const docFirst = docs[0];
+ docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)));
+ const preferredInd = NumCast(docFirst.cluster);
+ docs.map(doc => doc.cluster = -1);
+ docs.map(doc => this._clusterSets.map((set, i) => set.map(member => {
+ if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
+ docFirst.cluster = i;
+ }
+ })));
+ if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
+ docFirst.cluster = preferredInd;
+ }
+ this._clusterSets.map((set, i) => {
+ if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
+ docFirst.cluster = i;
+ }
+ });
+ if (docFirst.cluster === -1) {
+ docs.map(doc => {
+ doc.cluster = this._clusterSets.length;
+ this._clusterSets.push([doc]);
+ });
+ } else {
+ for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]);
+ docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc));
+ }
+ childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child));
+ childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster?.toString());
+ }
+ }
+
@undoBatch
@action
updateCluster(doc: Doc) {
- let childLayouts = this.childLayoutPairs.map(pair => pair.layout);
+ const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
if (this.props.Document.useClusters) {
this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
- let preferredInd = NumCast(doc.cluster);
+ const preferredInd = NumCast(doc.cluster);
doc.cluster = -1;
this._clusterSets.map((set, i) => set.map(member => {
if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
@@ -243,15 +287,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getClusterColor = (doc: Doc) => {
let clusterColor = "";
- let cluster = NumCast(doc.cluster);
+ const cluster = NumCast(doc.cluster);
if (this.Document.useClusters) {
if (this._clusterSets.length <= cluster) {
setTimeout(() => this.updateCluster(doc), 0);
} else {
// choose a cluster color from a palette
- let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+ const colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
clusterColor = colors[cluster % colors.length];
- let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
+ const set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
// override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document
set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
@@ -260,97 +304,394 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return clusterColor;
}
+
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble) return;
+ if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+ return;
+ }
this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
- if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
+ if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) {
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;
+ // if physically using a pen or we're in pen or highlighter mode
+ // if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+ // e.stopPropagation();
+ // e.preventDefault();
+ // const point = this.getTransform().transformPoint(e.pageX, e.pageY);
+ // this._points.push({ X: point[0], Y: point[1] });
+ // }
+ // if not using a pen and in no ink mode
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ }
+ // eraser or scrubber plus anything else mode
+ else {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ // if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
+ // document.removeEventListener("pointermove", this.onPointerMove);
+ // document.removeEventListener("pointerup", this.onPointerUp);
+ // document.addEventListener("pointermove", this.onPointerMove);
+ // document.addEventListener("pointerup", this.onPointerUp);
+ // if (InkingControl.Instance.selectedTool === InkTool.None) {
+ // this._lastX = e.pageX;
+ // this._lastY = e.pageY;
+ // }
+ // else {
+ // e.stopPropagation();
+ // e.preventDefault();
+
+ // if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
+ // let point = this.getTransform().transformPoint(e.pageX, e.pageY);
+ // this._points.push({ x: point[0], y: point[1] });
+ // }
+ // }
+ // }
+ }
+
+ @action
+ handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ if (!e.nativeEvent.cancelBubble) {
+ // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt = me.changedTouches[0];
+ if (pt) {
+ this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
+ if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) {
+ // e.stopPropagation();
+ // e.preventDefault();
+ // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY);
+ // this._points.push({ X: point[0], Y: point[1] });
+ // }
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ this._lastX = pt.pageX;
+ this._lastY = pt.pageY;
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ else {
+ e.preventDefault();
+ }
+ }
+ }
+ }
+ }
+
+ @undoBatch
+ onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
+ switch (ge.gesture) {
+ case GestureUtils.Gestures.Stroke:
+ const points = ge.points;
+ const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
+ const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
+ this.addDocument(inkDoc);
+ e.stopPropagation();
+ break;
+ case GestureUtils.Gestures.Box:
+ const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
+ const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
+ const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] };
+ const bWidth = bounds.r - bounds.x;
+ const bHeight = bounds.b - bounds.y;
+ const sel = this.getActiveDocuments().filter(doc => {
+ const l = NumCast(doc.x);
+ const r = l + doc[WidthSym]();
+ const t = NumCast(doc.y);
+ const b = t + doc[HeightSym]();
+ const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
+ if (pass) {
+ doc.x = l - bounds.x - bWidth / 2;
+ doc.y = t - bounds.y - bHeight / 2;
+ }
+ return pass;
+ });
+ this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 }));
+ sel.forEach(d => this.props.removeDocument(d));
+ e.stopPropagation();
+ break;
+ case GestureUtils.Gestures.StartBracket:
+ const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
+ this._inkToTextStartX = start[0];
+ this._inkToTextStartY = start[1];
+ console.log("start");
+ break;
+ case GestureUtils.Gestures.EndBracket:
+ console.log("end");
+ if (this._inkToTextStartX && this._inkToTextStartY) {
+ const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
+ const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "text" && s.color);
+ const sets = setDocs.map((sd) => {
+ return Cast(sd.data, RichTextField)?.Text as string;
+ });
+ if (sets.length && sets[0]) {
+ this._wordPalette.clear();
+ const colors = setDocs.map(sd => FieldValue(sd.color) as string);
+ sets.forEach((st: string, i: number) => {
+ const words = st.split(",");
+ words.forEach(word => {
+ this._wordPalette.set(word, colors[i]);
+ });
+ });
+ }
+ const inks = this.getActiveDocuments().filter(doc => {
+ if (doc.type === "ink") {
+ const l = NumCast(doc.x);
+ const r = l + doc[WidthSym]();
+ const t = NumCast(doc.y);
+ const b = t + doc[HeightSym]();
+ const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t);
+ return pass;
+ }
+ return false;
+ });
+ const inkFields = inks.map(i => Cast(i.data, InkField));
+ CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => {
+ const wordResults = results.filter((r: any) => r.category === "inkWord");
+ console.log(wordResults);
+ for (const word of wordResults) {
+ const indices: number[] = word.strokeIds;
+ indices.forEach(i => {
+ const otherInks: Doc[] = [];
+ indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2]));
+ inks[i].relatedInks = new List<Doc>(otherInks);
+ const uniqueColors: string[] = [];
+ Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c));
+ inks[i].alternativeColors = new List<string>(uniqueColors);
+ if (this._wordPalette.has(word.recognizedText)) {
+ inks[i].color = this._wordPalette.get(word.recognizedText);
+ }
+ else {
+ for (const alt of word.alternates) {
+ if (this._wordPalette.has(alt.recognizedString)) {
+ inks[i].color = this._wordPalette.get(alt.recognizedString);
+ break;
+ }
+ }
+ }
+ });
+ }
+ });
+ this._inkToTextStartX = end[0];
+ }
+ break;
+ case GestureUtils.Gestures.Text:
+ if (ge.text) {
+ const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y);
+ this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] }));
+ e.stopPropagation();
+ }
}
}
+ @action
onPointerUp = (e: PointerEvent): void => {
+ if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return;
+
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ this.removeMoveListeners();
+ this.removeEndListeners();
+ }
+
+ @action
+ pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => {
+ // I think it makes sense for the marquee menu to go away when panned. -syip2
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+
+ let x = this.Document._panX || 0;
+ let y = this.Document._panY || 0;
+ const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout);
+ const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+ if (!this.isAnnotationOverlay && docs.length) {
+ PDFMenu.Instance.fadeOut(true);
+ const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0;
+ const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0;
+ const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx;
+ const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny;
+ const ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ const x = this.childDataProvider(doc).x;//NumCast(doc.x);
+ const y = this.childDataProvider(doc).y;//NumCast(doc.y);
+ const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width);
+ const ye = this.childDataProvider(doc).height + y; //y + NumCast(layoutDoc.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]]);
+
+ const cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
+ const panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
+ this.props.PanelHeight() / this.zoomScaling() * cscale);
+ if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
+ if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
+ if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
+ if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
+ }
+ this.setPan(x - dx, y - dy);
+ this._lastX = e.clientX;
+ this._lastY = e.clientY;
}
@action
onPointerMove = (e: PointerEvent): void => {
+ if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+ if (this.props.active(true)) {
+ e.stopPropagation();
+ }
+ return;
+ }
+ if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ return;
+ }
if (!e.cancelBubble) {
- if (this._hitCluster && this.tryDragCluster(e)) {
- 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();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- return;
+ const selectedTool = InkingControl.Instance.selectedTool;
+ if (selectedTool === InkTool.None) {
+ if (this._hitCluster && this.tryDragCluster(e)) {
+ 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();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ return;
+ }
+ this.pan(e);
}
- let x = this.Document.panX || 0;
- let y = this.Document.panY || 0;
- let docs = this.childLayoutPairs.map(pair => pair.layout);
- 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(Doc.Layout(docs[0]).width) + minx : minx;
- let miny = docs.length ? NumCast(docs[0].y) : 0;
- let maxy = docs.length ? NumCast(Doc.Layout(docs[0]).height) + miny : miny;
- let ranges = docs.filter(doc => doc).reduce((range, doc) => {
- let layoutDoc = Doc.Layout(doc);
- let x = NumCast(doc.x);
- let xe = x + NumCast(layoutDoc.width);
- let y = NumCast(doc.y);
- let ye = y + NumCast(layoutDoc.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 = this.extensionDoc && Cast(this.extensionDoc.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)];
- });
+ 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();
+ }
+ }
+
+ handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
+ // panning a workspace
+ if (!e.cancelBubble) {
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt = myTouches[0];
+ if (pt) {
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ if (this._hitCluster && this.tryDragCluster(e)) {
+ 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();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ return;
+ }
+ this.pan(pt);
}
+ }
+ // e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
+ // pinch zooming
+ if (!e.cancelBubble) {
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt1 = myTouches[0];
+ const pt2 = myTouches[1];
+ console.log(myTouches);
+
+ if (this.prevPoints.size === 2) {
+ const oldPoint1 = this.prevPoints.get(pt1.identifier);
+ const oldPoint2 = this.prevPoints.get(pt2.identifier);
+ if (oldPoint1 && oldPoint2) {
+ const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
+
+ // if zooming, zoom
+ if (dir !== 0) {
+ const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
+ const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
+ const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+ const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+
+ // calculate the raw delta value
+ const rawDelta = (dir * (d1 + d2));
- let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
- let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
- this.props.PanelHeight() / this.zoomScaling() * cscale);
- if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
- if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
- if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
- if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
+ // this floors and ceils the delta value to prevent jitteriness
+ const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8);
+ this.zoom(centerX, centerY, delta * window.devicePixelRatio);
+ this.prevPoints.set(pt1.identifier, pt1);
+ this.prevPoints.set(pt2.identifier, pt2);
+ }
+ // this is not zooming. derive some form of panning from it.
+ else {
+ // use the centerx and centery as the "new mouse position"
+ const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+ const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+ this.pan({ clientX: centerX, clientY: centerY });
+ this._lastX = centerX;
+ this._lastY = centerY;
+ }
+ }
}
- 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.stopPropagation();
e.preventDefault();
}
}
@action
+ handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ if (!e.nativeEvent.cancelBubble && this.props.active(true)) {
+ // const pt1: React.Touch | null = e.targetTouches.item(0);
+ // const pt2: React.Touch | null = e.targetTouches.item(1);
+ // // if (!pt1 || !pt2) return;
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt1 = myTouches[0];
+ const pt2 = myTouches[1];
+ if (pt1 && pt2) {
+ const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+ const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+ this._lastX = centerX;
+ this._lastY = centerY;
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ e.stopPropagation();
+ }
+ }
+ }
+
+ cleanUpInteractions = () => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ this.removeMoveListeners();
+ this.removeEndListeners();
+ }
+
+ @action
+ zoom = (pointX: number, pointY: number, deltaY: number): void => {
+ let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1;
+ if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
+ deltaScale = 1 / this.zoomScaling();
+ }
+ if (deltaScale < 0) deltaScale = -deltaScale;
+ const [x, y] = this.getTransform().transformPoint(pointX, pointY);
+ const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
+
+ if (localTransform.Scale >= 0.15) {
+ const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
+ this.props.Document.scale = Math.abs(safeScale);
+ this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+ }
+ }
+
+ @action
onPointerWheel = (e: React.WheelEvent): void => {
if (this.props.Document.lockedTransform || this.props.Document.inOverlay) return;
if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
e.stopPropagation();
}
- else if (this.props.active()) {
+ else if (this.props.active(true)) {
e.stopPropagation();
- let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
- 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.min(Math.max(0.15, localTransform.Scale), 40);
- this.props.Document.scale = Math.abs(safeScale);
- this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+ this.zoom(e.clientX, e.clientY, e.deltaY);
}
}
@@ -358,11 +699,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
setPan(panX: number, panY: number, panType: string = "None") {
if (!this.Document.lockedTransform || this.Document.inOverlay) {
this.Document.panTransformType = panType;
- var scale = this.getLocalTransform().inverse().Scale;
+ const scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
- this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ this.Document._panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.Document._panY = this.isAnnotationOverlay ? newPanY : panY;
}
}
@@ -383,48 +724,50 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => {
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" && this.Document.panX !== undefined && this.Document.panY !== undefined) {
+ if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) {
const init = state.initializers![this.Document[Id]];
if (!init) {
- state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY };
+ state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY };
HistoryUtil.pushState(state);
- } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) {
- init.panX = this.Document.panX;
- init.panY = this.Document.panY;
+ } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) {
+ init.panX = this.Document._panX;
+ init.panY = this.Document._panY;
HistoryUtil.pushState(state);
}
}
SelectionManager.DeselectAll();
if (this.props.Document.scrollHeight) {
- let annotOn = Cast(doc.annotationOn, Doc) as Doc;
+ const annotOn = Cast(doc.annotationOn, Doc) as Doc;
if (!annotOn) {
this.props.focus(doc);
} else {
- let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
- let offset = annotOn && (contextHgt / 2 * 96 / 72);
+ const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
+ const offset = annotOn && (contextHgt / 2 * 96 / 72);
this.props.Document.scrollY = NumCast(doc.y) - offset;
}
} else {
- let layoutdoc = Doc.Layout(doc);
- const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
+ const layoutdoc = Doc.Layout(doc);
+ const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2;
const newState = HistoryUtil.getState();
newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
- let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
+ const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType };
- this.setPan(newPanX, newPanY, "Ease");
+ if (!doc.z) this.setPan(newPanX, newPanY, "Ease"); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
Doc.BrushDoc(this.props.Document);
this.props.focus(this.props.Document);
willZoom && this.setScaleToZoom(layoutdoc, scale);
+ Doc.linkFollowHighlight(doc);
afterFocus && setTimeout(() => {
if (afterFocus && afterFocus()) {
- this.Document.panX = savedState.px;
- this.Document.panY = savedState.py;
+ this.Document._panX = savedState.px;
+ this.Document._panY = savedState.py;
this.Document.scale = savedState.s;
this.Document.panTransformType = savedState.pt;
}
@@ -434,7 +777,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
- this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height));
+ this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
}
zoomToScale = (scale: number) => {
@@ -443,14 +786,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getScale = () => this.Document.scale || 1;
+ @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
...this.props,
DataDoc: childData,
Document: childLayout,
+ LibraryPath: this.libraryPath,
layoutKey: undefined,
- ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves
- onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
+ //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
+ onClick: this.onChildClickHandler,
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
PanelWidth: childLayout[WidthSym],
@@ -468,13 +815,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } {
- const script = this.Document.arrangeScript;
- const result = script && script.script.run(params, console.log);
- const layoutDoc = Doc.Layout(params.doc);
- if (result && result.success) {
+ const result = this.Document.arrangeScript?.script.run(params, console.log);
+ if (result?.success) {
return { ...result, transition: "transform 1s" };
}
- return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") };
+ const layoutDoc = Doc.Layout(params.doc);
+ return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") };
}
viewDefsToJSX = (views: any[]) => {
@@ -492,7 +838,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const fontSize = Cast(viewDef.fontSize, "number");
return [text, x, y, width, height].some(val => val === undefined) ? undefined :
{
- ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
+ ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z} style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
{text}
</div>,
bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
@@ -500,64 +846,64 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
- lookupLayout = (doc: Doc, dataDoc?: Doc) => {
- let data: any = undefined;
- let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
- switch (this.Document.freeformLayoutEngine) {
- case "pivot": computedElementData = this.doPivotLayout; break;
- default: computedElementData = this.doFreeformLayout; break;
+ childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) {
+ if (!doc) {
+ console.log(doc);
}
- computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => {
- if (key.layout === doc && key.data === dataDoc) {
- data = value;
- }
- });
- return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition };
- }
+ return this._layoutPoolData.get(doc[Id]);
+ }.bind(this));
- @computed
- get doPivotLayout() {
- return computePivotLayout(this.props.Document, this.childDocs,
- this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
+ doPivotLayout(poolData: ObservableMap<string, any>) {
+ return computePivotLayout(poolData, this.props.Document, this.childDocs,
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
- @computed
- get doFreeformLayout() {
- let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
- let layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
+ doFreeformLayout(poolData: ObservableMap<string, any>) {
+ const layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
let state = initResult && initResult.success ? initResult.result.scriptState : undefined;
- let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
+ const elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
+ const data = poolData.get(pair.layout[Id]);
const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
state = pos.state === undefined ? state : pos.state;
- layoutPoolData.set(pair, pos);
+ if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height || pos.transition !== data.transition) {
+ runInAction(() => poolData.set(pair.layout[Id], pos));
+ }
});
- return { map: layoutPoolData, elements: elements };
+ return { elements: elements };
}
- @computed
get doLayoutComputation() {
- let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
- switch (this.Document.freeformLayoutEngine) {
- case "pivot": computedElementData = this.doPivotLayout; break;
- default: computedElementData = this.doFreeformLayout; break;
+ let computedElementData: { elements: ViewDefResult[] };
+ switch (this.Document._freeformLayoutEngine) {
+ case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break;
+ default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break;
}
- this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
+ this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair =>
computedElementData.elements.push({
- ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout}
- ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
- jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
- bounds: this.lookupLayout(pair.layout, pair.data)
+ ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
+ dataProvider={this.childDataProvider}
+ jitterRotation={NumCast(this.props.Document.jitterRotation)}
+ fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />,
+ bounds: this.childDataProvider(pair.layout)
}));
return computedElementData;
}
- @computed.struct get elements() { return this.doLayoutComputation.elements; }
- @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
- @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); }
+ componentDidMount() {
+ super.componentDidMount();
+ this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation; },
+ action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)),
+ { fireImmediately: true, name: "doLayout" });
+ }
+ componentWillUnmount() {
+ this._layoutComputeReaction && this._layoutComputeReaction();
+ }
+ @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
+ elementFunc = () => this._layoutElements;
@action
onCursorMove = (e: React.PointerEvent) => {
@@ -567,12 +913,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
layoutDocsInGrid = () => {
UndoManager.RunInBatch(() => {
const docs = DocListCast(this.Document[this.props.fieldKey]);
- let startX = this.Document.panX || 0;
+ const startX = this.Document._panX || 0;
let x = startX;
- let y = this.Document.panY || 0;
+ let y = this.Document._panY || 0;
let i = 0;
- const width = Math.max(...docs.map(doc => NumCast(doc.width)));
- const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+ const width = Math.max(...docs.map(doc => NumCast(doc._width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc._height)));
for (const doc of docs) {
doc.x = x;
doc.y = y;
@@ -586,41 +932,58 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}, "arrange contents");
}
- autoFormat = () => {
- this.Document.isRuleProvider = !this.Document.isRuleProvider;
- // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading.
- this.Document.isRuleProvider && this.childLayoutPairs.map(pair =>
- // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template)
- DocListCast(Doc.Layout(pair.layout).data).map(heading => {
- let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
- let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
- if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) {
- Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor;
- }
- })
- );
- }
+ private thumbIdentifier?: number;
- analyzeStrokes = async () => {
- const extensionDoc = this.extensionDoc;
- let data = extensionDoc && Cast(extensionDoc.ink, InkField);
- if (data && extensionDoc) {
- CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
- }
- }
+ // @action
+ // handleHandDown = (e: React.TouchEvent) => {
+ // const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+ // const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+ // this.thumbIdentifier = thumb?.identifier;
+ // const others = fingers.filter(f => f !== thumb);
+ // const minX = Math.min(...others.map(f => f.clientX));
+ // const minY = Math.min(...others.map(f => f.clientY));
+ // const t = this.getTransform().transformPoint(minX, minY);
+ // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
+
+ // const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+ // if (thumbDoc) {
+ // this._palette = <Palette x={t[0]} y={t[1]} thumb={th} thumbDoc={thumbDoc} />;
+ // }
+
+ // document.removeEventListener("touchmove", this.onTouch);
+ // document.removeEventListener("touchmove", this.handleHandMove);
+ // document.addEventListener("touchmove", this.handleHandMove);
+ // document.removeEventListener("touchend", this.handleHandUp);
+ // document.addEventListener("touchend", this.handleHandUp);
+ // }
+
+ // @action
+ // handleHandMove = (e: TouchEvent) => {
+ // for (let i = 0; i < e.changedTouches.length; i++) {
+ // const pt = e.changedTouches.item(i);
+ // if (pt?.identifier === this.thumbIdentifier) {
+ // }
+ // }
+ // }
+
+ // @action
+ // handleHandUp = (e: TouchEvent) => {
+ // this.onTouchEnd(e);
+ // if (this.prevPoints.size < 3) {
+ // this._palette = undefined;
+ // document.removeEventListener("touchend", this.handleHandUp);
+ // }
+ // }
onContextMenu = (e: React.MouseEvent) => {
- let layoutItems: ContextMenuProps[] = [];
+ const layoutItems: ContextMenuProps[] = [];
- if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) {
- layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
- }
- layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
- layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+ layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
+ layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
- layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" });
layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
- layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+ // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
layoutItems.push({
description: "Import document", icon: "upload", event: ({ x, y }) => {
@@ -629,7 +992,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
input.accept = ".zip";
input.onchange = async _e => {
const upload = Utils.prepend("/uploadDoc");
- let formData = new FormData();
+ const formData = new FormData();
const file = input.files && input.files[0];
if (file) {
formData.append('file', file);
@@ -654,7 +1017,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
description: "Add Note ...",
subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
description: (i + 1) + ": " + StrCast(note.title),
- event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })),
+ event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument("", { _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], _autoHeight: true, layout: note, title: StrCast(note.title) })),
icon: "eye"
})) as ContextMenuProps[],
icon: "eye"
@@ -664,37 +1027,80 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private childViews = () => {
- let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
+ const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
return [
...children,
...this.views,
];
}
+
+ // @observable private _palette?: JSX.Element;
+
+ children = () => {
+ const eles: JSX.Element[] = [];
+ eles.push(...this.childViews());
+ // this._palette && (eles.push(this._palette));
+ // this.currentStroke && (eles.push(this.currentStroke));
+ eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
+ return eles;
+ }
+ @computed get placeholder() {
+ return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}>
+ <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span>
+ </div>;
+ }
+ @computed get marqueeView() {
+ return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+ addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
+ <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
+ easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ {this.children}
+ </CollectionFreeFormViewPannableContents>
+ </MarqueeView>;
+ }
+
+ @computed get contentScaling() {
+ if (this.props.annotationsKey) return 0;
+ const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
+ const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1;
+ return wscale < hscale ? wscale : hscale;
+ }
render() {
+ TraceMobx();
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
- this.Document.fitX = this.contentBounds && this.contentBounds.x;
- this.Document.fitY = this.contentBounds && this.contentBounds.y;
- this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
- this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
+ // this.Document.fitX = this.contentBounds && this.contentBounds.x;
+ // this.Document.fitY = this.contentBounds && this.contentBounds.y;
+ // this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
+ // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
// if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
- return !this.extensionDoc ? (null) :
- <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
- style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
- <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
- addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
- <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
- easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
- {!this.extensionDoc ? (null) :
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
- {this.childViews}
- </InkingCanvas>}
- <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
- </CollectionFreeFormViewPannableContents>
- </MarqueeView>
- {this.overlayViews}
- </div>;
+ // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
+ return <div className={"collectionfreeformview-container"}
+ ref={this.createDashEventsTarget}
+ onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}
+ style={{
+ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+ transform: this.contentScaling ? `scale(${this.contentScaling})` : "",
+ transformOrigin: this.contentScaling ? "left top" : "",
+ width: this.contentScaling ? `${100 / this.contentScaling}%` : "",
+ height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
+ }}>
+ {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
+ this.placeholder : this.marqueeView}
+ <CollectionFreeFormOverlayView elements={this.elementFunc} />
+ </div>;
+ }
+}
+
+interface CollectionFreeFormOverlayViewProps {
+ elements: () => ViewDefResult[];
+}
+
+@observer
+class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOverlayViewProps>{
+ render() {
+ return this.props.elements().filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
}
}
@@ -705,20 +1111,20 @@ interface CollectionFreeFormViewPannableContentsProps {
panY: () => number;
zoomScaling: () => number;
easing: () => boolean;
+ children: () => JSX.Element[];
}
@observer
class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{
render() {
- let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none");
+ const 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();
- return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
- {this.props.children}
- {/* <ClientRecommender title="Distance Matrix" /> */}
+ return <div className={freeformclass} style={{ touchAction: "none", borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
+ {this.props.children()}
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
new file mode 100644
index 000000000..db4b674b5
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -0,0 +1,57 @@
+import React = require("react");
+import AntimodeMenu from "../../AntimodeMenu";
+import { observer } from "mobx-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { unimplementedFunction } from "../../../../Utils";
+
+@observer
+export default class MarqueeOptionsMenu extends AntimodeMenu {
+ static Instance: MarqueeOptionsMenu;
+
+ public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public showMarquee: () => void = unimplementedFunction;
+ public hideMarquee: () => void = unimplementedFunction;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ MarqueeOptionsMenu.Instance = this;
+ }
+
+ render() {
+ const buttons = [
+ <button
+ className="antimodeMenu-button"
+ title="Create a Collection"
+ key="group"
+ onPointerDown={this.createCollection}>
+ <FontAwesomeIcon icon="object-group" size="lg" />
+ </button>,
+ <button
+ className="antimodeMenu-button"
+ title="Summarize Documents"
+ key="summarize"
+ onPointerDown={this.summarize}>
+ <FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
+ </button>,
+ <button
+ className="antimodeMenu-button"
+ title="Delete Documents"
+ key="delete"
+ onPointerDown={this.delete}>
+ <FontAwesomeIcon icon="trash-alt" size="lg" />
+ </button>,
+ <button
+ className="antimodeMenu-button"
+ title="Change to Text"
+ key="inkToText"
+ onPointerDown={this.inkToText}>
+ <FontAwesomeIcon icon="font" size="lg" />
+ </button>,
+ ];
+ return this.getElement(buttons);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 04f6ec2ad..18d6da0da 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -5,9 +5,9 @@
left:0;
width:100%;
height:100%;
-}
-.marqueeView {
overflow: hidden;
+ pointer-events: inherit;
+ border-radius: inherit;
}
.marqueeView:focus-within {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 44b6fe030..19a71012a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,36 +1,36 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../../new_fields/Doc";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { InkField } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
import { listSpec } from "../../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
import { ComputedField } from "../../../../new_fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../../Utils";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
-import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
import { CollectionViewType } from "../CollectionView";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
+import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { SubCollectionViewProps } from "../CollectionSubView";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { RichTextField } from "../../../../new_fields/RichTextField";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
getTransform: () => Transform;
addDocument: (doc: Doc) => boolean;
activeDocuments: () => Doc[];
- selectDocuments: (docs: Doc[]) => void;
+ selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void;
removeDocument: (doc: Doc) => boolean;
addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
- extensionDoc: Doc;
isAnnotationOverlay?: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
@@ -51,39 +51,42 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
@action
- cleanupInteractions = (all: boolean = false) => {
+ cleanupInteractions = (all: boolean = false, hideMarquee: boolean = true) => {
if (all) {
document.removeEventListener("pointerup", this.onPointerUp, true);
document.removeEventListener("pointermove", this.onPointerMove, true);
}
document.removeEventListener("keydown", this.marqueeCommand, true);
- this._visible = false;
+ if (hideMarquee) {
+ this._visible = false;
+ }
}
@undoBatch
@action
onKeyPress = (e: KeyboardEvent) => {
//make textbox and add it to this collection
+ // tslint:disable-next-line:prefer-const
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() !== "");
+ const text: string = await navigator.clipboard.readText();
+ const 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() === "";
+ const sub = ns[i].endsWith("\r") ? 1 : 0;
+ const 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.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ const indent = line.search(/\S|$/);
+ const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: y, title: line });
this.props.addDocument(newBox);
y += 40 * this.props.getTransform().Scale;
});
@@ -91,19 +94,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
} else if (e.key === "b" && e.ctrlKey) {
e.preventDefault();
navigator.clipboard.readText().then(text => {
- let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
if (ns.length === 1 && text.startsWith("http")) {
- this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
+ this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
} else {
this.pasteTable(ns, x, y);
}
});
} else if (!e.ctrlKey) {
this.props.addLiveTextDocument(
- Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
+ Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }));
} else if (e.keyCode > 48 && e.keyCode <= 57) {
- let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
- let text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
+ const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
+ const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" });
text.layout = notes[(e.keyCode - 49) % notes.length];
this.props.addLiveTextDocument(text);
}
@@ -121,31 +124,31 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
ns.splice(0, 1);
}
if (ns.length > 0) {
- let columns = ns[0].split("\t");
- let docList: Doc[] = [];
+ const columns = ns[0].split("\t");
+ const docList: Doc[] = [];
let groupAttr: string | number = "";
- let rowProto = new Doc();
+ const rowProto = new Doc();
rowProto.title = rowProto.Id;
- rowProto.width = 200;
+ rowProto._width = 200;
rowProto.isPrototype = true;
for (let i = 1; i < ns.length - 1; i++) {
- let values = ns[i].split("\t");
+ const values = ns[i].split("\t");
if (values.length === 1 && columns.length > 1) {
groupAttr = values[0];
continue;
}
- let docDataProto = Doc.MakeDelegate(rowProto);
+ const docDataProto = Doc.MakeDelegate(rowProto);
docDataProto.isPrototype = true;
columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
if (groupAttr) {
docDataProto._group = groupAttr;
}
docDataProto.title = i.toString();
- let doc = Doc.MakeDelegate(docDataProto);
- doc.width = 200;
+ const doc = Doc.MakeDelegate(docDataProto);
+ doc._width = 200;
docList.push(doc);
}
- let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 });
this.props.addDocument(newCol);
}
@@ -188,15 +191,34 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@action
onPointerUp = (e: PointerEvent): void => {
- if (!this.props.active()) this.props.selectDocuments([this.props.Document]);
+ if (!this.props.active(true)) this.props.selectDocuments([this.props.Document], []);
if (this._visible) {
- let mselect = this.marqueeSelect();
+ const mselect = this.marqueeSelect();
if (!e.shiftKey) {
SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
}
- this.props.selectDocuments(mselect.length ? mselect : [this.props.Document]);
+ // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map();
+ // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : [];
+ const docs = mselect.length ? mselect : [this.props.Document];
+ this.props.selectDocuments(docs, []);
}
- this.cleanupInteractions(true);
+ if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) {
+ MarqueeOptionsMenu.Instance.createCollection = this.collection;
+ MarqueeOptionsMenu.Instance.delete = this.delete;
+ MarqueeOptionsMenu.Instance.summarize = this.summary;
+ MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight;
+ MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
+ MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
+ MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ this.cleanupInteractions(true, this._commandExecuted);
+
+ const hideMarquee = () => {
+ this.hideMarquee();
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ document.removeEventListener("pointerdown", hideMarquee);
+ };
+ document.addEventListener("pointerdown", hideMarquee);
if (e.altKey) {
e.preventDefault();
@@ -239,19 +261,174 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@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);
+ const left = this._downX < this._lastX ? this._downX : this._lastX;
+ const top = this._downY < this._lastY ? this._downY : this._lastY;
+ const topLeft = this.props.getTransform().transformPoint(left, top);
+ const 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]) };
}
+ get inkDoc() {
+ return this.props.Document;
+ }
+
get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
- return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
+ return Cast(this.props.Document.ink, InkField);
}
set ink(value: InkField | undefined) {
- this.props.extensionDoc && (this.props.extensionDoc.ink = value);
+ this.props.Document.ink = value;
+ }
+
+ @action
+ showMarquee = () => {
+ this._visible = true;
+ }
+
+ @action
+ hideMarquee = () => {
+ this._visible = false;
+ }
+
+ @action
+ delete = () => {
+ this.marqueeSelect(false).map(d => this.props.removeDocument(d));
+ if (this.ink) {
+ // this.marqueeInkDelete(this.ink.inkData);
+ }
+ SelectionManager.DeselectAll();
+ this.cleanupInteractions(false);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ this.hideMarquee();
+ }
+
+ getCollection = (selected: Doc[], asTemplate: boolean) => {
+ const bounds = this.Bounds;
+ const defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+ "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
+ const colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
+ if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
+ const palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
+ const usedPaletted = new Map<string, number>();
+ [...this.props.activeDocuments(), this.props.Document].map(child => {
+ const bg = StrCast(Doc.Layout(child).backgroundColor);
+ if (palette.indexOf(bg) !== -1) {
+ palette.splice(palette.indexOf(bg), 1);
+ if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
+ else usedPaletted.set(bg, 1);
+ }
+ });
+ usedPaletted.delete("#f1efeb");
+ usedPaletted.delete("white");
+ usedPaletted.delete("rgba(255,255,255,1)");
+ const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
+ const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
+ // const inkData = this.ink ? this.ink.inkData : undefined;
+ const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument;
+ const newCollection = creator(selected, {
+ x: bounds.left,
+ y: bounds.top,
+ _panX: 0,
+ _panY: 0,
+ backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+ defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+ _width: bounds.width,
+ _height: bounds.height,
+ _LODdisable: true,
+ title: "a nested collection",
+ });
+ // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
+ // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
+ // this.marqueeInkDelete(inkData);
+ this.hideMarquee();
+ return newCollection;
+ }
+
+ @action
+ collection = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const bounds = this.Bounds;
+ const selected = this.marqueeSelect(false);
+ if (e instanceof KeyboardEvent ? e.key === "c" : true) {
+ 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.displayTimecode = undefined;
+ return d;
+ });
+ }
+ const newCollection = this.getCollection(selected, e.key === "t");
+ this.props.addDocument(newCollection);
+ this.props.selectDocuments([newCollection], []);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ this.hideMarquee();
+ }
+
+ @action
+ syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const selected = this.marqueeSelect(false);
+ if (e instanceof KeyboardEvent ? e.key === "i" : true) {
+ const inks = selected.filter(s => s.proto?.type === "ink");
+ const setDocs = selected.filter(s => s.proto?.type === "text" && s.color);
+ const sets = setDocs.map((sd) => {
+ return Cast(sd.data, RichTextField)?.Text as string;
+ });
+ const colors = setDocs.map(sd => FieldValue(sd.color) as string);
+ const wordToColor = new Map<string, string>();
+ console.log(sets);
+ sets.forEach((st: string, i: number) => {
+ const words = st.split(",");
+ words.forEach(word => {
+ wordToColor.set(word, colors[i]);
+ });
+ });
+ const inkFields = inks.map(i => Cast(i.data, InkField));
+ CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => {
+ const wordResults = results.filter((r: any) => r.category === "inkWord");
+ console.log(wordResults);
+ for (const word of wordResults) {
+ const indices: number[] = word.strokeIds;
+ indices.forEach(i => {
+ if (wordToColor.has(word.recognizedText)) {
+ inks[i].color = wordToColor.get(word.recognizedText);
+ }
+ })
+ }
+ });
+ }
+ }
+
+ @action
+ summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const bounds = this.Bounds;
+ const selected = this.marqueeSelect(false);
+ const newCollection = this.getCollection(selected);
+
+ 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;
+ });
+ newCollection._chromeStatus = "disabled";
+ const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 300, _height: 100, _autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
+ newCollection.x = bounds.left + bounds.width;
+ Doc.GetProto(newCollection).summaryDoc = summary;
+ Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
+ if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
+ const container = Docs.Create.FreeformDocument([summary, newCollection], {
+ x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true,
+ _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-"
+ });
+ Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight"
+ this.props.addLiveTextDocument(container);
+ } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
+ Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ this.props.addLiveTextDocument(summary);
+ }
+ MarqueeOptionsMenu.Instance.fadeOut(true);
}
@undoBatch
@@ -264,168 +441,101 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._commandExecuted = true;
e.stopPropagation();
(e as any).propagationIsStopped = true;
- this.marqueeSelect(false).map(d => this.props.removeDocument(d));
- if (this.ink) {
- this.marqueeInkDelete(this.ink.inkData);
- }
- SelectionManager.DeselectAll();
- this.cleanupInteractions(false);
+ this.delete();
e.stopPropagation();
}
- if (e.key === "c" || e.key === "s" || e.key === "S") {
+ if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") {
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
(e as any).propagationIsStopped = true;
- let bounds = this.Bounds;
- let selected = this.marqueeSelect(false);
- 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.displayTimecode = undefined;
- return d;
- });
+ if (e.key === "c" || e.key === "t") {
+ this.collection(e);
}
- let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
- "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
- let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
- if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
- let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
- let usedPaletted = new Map<string, number>();
- [...this.props.activeDocuments(), this.props.Document].map(child => {
- let bg = StrCast(Doc.Layout(child).backgroundColor);
- if (palette.indexOf(bg) !== -1) {
- palette.splice(palette.indexOf(bg), 1);
- if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
- else usedPaletted.set(bg, 1);
- }
- });
- usedPaletted.delete("#f1efeb");
- usedPaletted.delete("white");
- usedPaletted.delete("rgba(255,255,255,1)");
- let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
- let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
- let inkData = this.ink ? this.ink.inkData : undefined;
- let newCollection = Docs.Create.FreeformDocument(selected, {
- x: bounds.left,
- y: bounds.top,
- panX: 0,
- panY: 0,
- backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
- defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
- width: bounds.width,
- height: bounds.height,
- title: "a nested collection",
- });
- let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
- dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
- this.marqueeInkDelete(inkData);
if (e.key === "s" || e.key === "S") {
- 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;
- });
- newCollection.chromeStatus = "disabled";
- let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
- Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
- newCollection.x = bounds.left + bounds.width;
- Doc.GetProto(newCollection).summaryDoc = summary;
- Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
- if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
- let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
- container.viewType = CollectionViewType.Stacking;
- container.autoHeight = true;
- Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight"
- this.props.addLiveTextDocument(container);
- } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
- Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight"
- this.props.addLiveTextDocument(summary);
- }
- }
- else {
- this.props.addDocument(newCollection);
- this.props.selectDocuments([newCollection]);
+ this.summary(e);
}
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
+ // 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: PointData, key: string, map: any) => {
+ // if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+ // // let transform = this.props.container.props.ScreenToLocalTransform().scale(this.props.container.props.ContentScaling());
+ // idata.set(key,
+ // {
+ // pathData: value.pathData.map(val => {
+ // let tVal = this.props.getTransform().inverse().transformPoint(val.x, val.y);
+ // return { x: tVal[0], y: tVal[1] };
+ // // return { x: val.x + centerShiftX, y: val.y + centerShiftY }
+ // }),
+ // color: value.color,
+ // width: value.width,
+ // tool: value.tool,
+ // page: -1
+ // });
+ // }
+ // });
+ // // InkSelectDecorations.Instance.SetSelected(idata);
+ // 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.ink = new InkField(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: PointData, key: string, map: any) =>
+ // !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
+ // this.ink = new InkField(idata);
+ // }
+ // }
marqueeSelect(selectBackgrounds: boolean = true) {
- let selRect = this.Bounds;
- let selection: Doc[] = [];
+ const selRect = this.Bounds;
+ const selection: Doc[] = [];
this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
- let layoutDoc = Doc.Layout(doc);
- var x = NumCast(doc.x);
- var y = NumCast(doc.y);
- var w = NumCast(layoutDoc.width);
- var h = NumCast(layoutDoc.height);
+ const layoutDoc = Doc.Layout(doc);
+ const x = NumCast(doc.x);
+ const y = NumCast(doc.y);
+ const w = NumCast(layoutDoc._width);
+ const h = NumCast(layoutDoc._height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
});
if (!selection.length && selectBackgrounds) {
this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
- let layoutDoc = Doc.Layout(doc);
- var x = NumCast(doc.x);
- var y = NumCast(doc.y);
- var w = NumCast(layoutDoc.width);
- var h = NumCast(layoutDoc.height);
+ const layoutDoc = Doc.Layout(doc);
+ const x = NumCast(doc.x);
+ const y = NumCast(doc.y);
+ const w = NumCast(layoutDoc._width);
+ const h = NumCast(layoutDoc._height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
});
}
if (!selection.length) {
- let left = this._downX < this._lastX ? this._downX : this._lastX;
- let top = this._downY < this._lastY ? this._downY : this._lastY;
- let topLeft = this.props.getContainerTransform().transformPoint(left, top);
- let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
+ const left = this._downX < this._lastX ? this._downX : this._lastX;
+ const top = this._downY < this._lastY ? this._downY : this._lastY;
+ const topLeft = this.props.getContainerTransform().transformPoint(left, top);
+ const size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ const otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
- let layoutDoc = Doc.Layout(doc);
- var x = NumCast(doc.x);
- var y = NumCast(doc.y);
- var w = NumCast(layoutDoc.width);
- var h = NumCast(layoutDoc.height);
+ const layoutDoc = Doc.Layout(doc);
+ const x = NumCast(doc.x);
+ const y = NumCast(doc.y);
+ const w = NumCast(layoutDoc._width);
+ const h = NumCast(layoutDoc._height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
selection.push(doc);
}
@@ -436,15 +546,20 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@computed
get marqueeDiv() {
- let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
- let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ const p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
+ const v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ /**
+ * @RE - The commented out span below
+ * This contains the "C for collection, ..." text on marquees.
+ * Commented out by syip2 when the marquee menu was added.
+ */
return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
- <span className="marquee-legend" />
+ {/* <span className="marquee-legend" /> */}
</div>;
}
render() {
- return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}>
{this._visible ? this.marqueeDiv : null}
{this.props.children}
</div>;