aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionFreeForm
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-08-19 10:11:59 -0400
committerbob <bcz@cs.brown.edu>2019-08-19 10:11:59 -0400
commite37bf9124c952aa26c3e29deb9e4faa01cad1a7e (patch)
treebe44ae9bd5e2eb6c5ce392383d41505b5863d061 /src/client/views/collections/collectionFreeForm
parent07482c3bf435748140addfd4fd338fc668657798 (diff)
parentb037aa89fb564812f880994453ce002054a0ad82 (diff)
Merge branch 'master' into presentation_f
Diffstat (limited to 'src/client/views/collections/collectionFreeForm')
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx563
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx45
3 files changed, 465 insertions, 144 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index cca199afa..c4311fa52 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -46,6 +46,7 @@
border-radius: inherit;
box-sizing: border-box;
position: absolute;
+ overflow: hidden;
.marqueeView {
overflow: hidden;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d70022280..6320cb3d5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,45 +1,44 @@
-import { action, computed, trace } from "mobx";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faEye } from "@fortawesome/free-regular-svg-icons";
+import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faChalkboard, faBraille } from "@fortawesome/free-solid-svg-icons";
+import { action, computed, observable, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc";
+import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast, FieldResult, Field, Opt } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { ScriptField } from "../../../../new_fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { emptyFunction, returnOne } from "../../../../Utils";
+import { emptyFunction, returnOne, Utils, returnFalse, returnEmptyString } from "../../../../Utils";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { DocServer } from "../../../DocServer";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
+import { CompileScript } from "../../../util/Scripting";
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 { SubmenuProps, ContextMenuProps } from "../../ContextMenuItem";
+import { ContextMenu } from "../../ContextMenu";
+import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingCanvas } from "../../InkingCanvas";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
import { pageSchema } from "../../nodes/ImageBox";
+import { OverlayElementOptions, OverlayView } from "../../OverlayView";
import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
+import { ScriptBox } from "../../ScriptBox";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import v5 = require("uuid/v5");
-import { ScriptField } from "../../../../new_fields/ScriptField";
-import { OverlayView, OverlayElementOptions } from "../../OverlayView";
-import { ScriptBox } from "../../ScriptBox";
-import { CompileScript } from "../../../util/Scripting";
-import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faEye } from "@fortawesome/free-regular-svg-icons";
-import { faTable, faPaintBrush, faAsterisk, faExpandArrowsAlt, faCompressArrowsAlt, faCompass } from "@fortawesome/free-solid-svg-icons";
-import { undo } from "prosemirror-history";
-import { number } from "prop-types";
-import { ContextMenu } from "../../ContextMenu";
+import { DocumentType, Docs } from "../../../documents/Documents";
-library.add(faEye, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass);
+library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard);
export const panZoomSchema = createSchema({
panX: "number",
@@ -49,6 +48,129 @@ export const panZoomSchema = createSchema({
arrangeInit: ScriptField,
});
+export interface ViewDefBounds {
+ x: number;
+ y: number;
+ z?: number;
+ width: number;
+ height: number;
+}
+
+export interface ViewDefResult {
+ ele: JSX.Element;
+ bounds?: ViewDefBounds;
+}
+
+export namespace PivotView {
+
+ export interface PivotData {
+ type: string;
+ text: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ fontSize: number;
+ }
+
+ export const elements = (target: CollectionFreeFormView) => {
+ let collection = target.Document;
+ const field = StrCast(collection.pivotField) || "title";
+ const width = NumCast(collection.pivotWidth) || 200;
+ const groups = new Map<FieldResult<Field>, Doc[]>();
+
+ for (const doc of target.childDocs) {
+ const val = doc[field];
+ if (val === undefined) continue;
+
+ const l = groups.get(val);
+ if (l) {
+ l.push(doc);
+ } else {
+ groups.set(val, [doc]);
+ }
+ }
+
+ let minSize = Infinity;
+
+ groups.forEach((val, key) => minSize = Math.min(minSize, val.length));
+
+ const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize));
+ const fontSize = NumCast(collection.pivotFontSize);
+
+ const docMap = new Map<Doc, ViewDefBounds>();
+ const groupNames: PivotData[] = [];
+
+ let x = 0;
+ groups.forEach((val, key) => {
+ let y = 0;
+ let xCount = 0;
+ groupNames.push({
+ type: "text",
+ text: String(key),
+ x,
+ y: width + 50,
+ width: width * 1.25 * numCols,
+ height: 100, fontSize: fontSize
+ });
+ for (const doc of val) {
+ docMap.set(doc, {
+ x: x + xCount * width * 1.25,
+ y: -y,
+ width,
+ height: width
+ });
+ xCount++;
+ if (xCount >= numCols) {
+ xCount = 0;
+ y += width * 1.25;
+ }
+ }
+ x += width * 1.25 * (numCols + 1);
+ });
+
+ let elements = target.viewDefsToJSX(groupNames);
+ let docViews = target.childDocs.reduce((prev, doc) => {
+ let minim = BoolCast(doc.isMinimized);
+ if (minim === undefined || !minim) {
+ let defaultPosition = (): ViewDefBounds => {
+ return {
+ x: NumCast(doc.x),
+ y: NumCast(doc.y),
+ z: NumCast(doc.z),
+ width: NumCast(doc.width),
+ height: NumCast(doc.height)
+ };
+ };
+ const pos = docMap.get(doc) || defaultPosition();
+ prev.push({
+ ele: <CollectionFreeFormDocumentView
+ key={doc[Id]}
+ x={pos.x}
+ y={pos.y}
+ width={pos.width}
+ height={pos.height}
+ {...target.getChildDocumentViewProps(doc)}
+ />,
+ bounds: {
+ x: pos.x,
+ y: pos.y,
+ z: pos.z,
+ width: NumCast(pos.width),
+ height: NumCast(pos.height)
+ }
+ });
+ }
+ return prev;
+ }, elements);
+
+ target.resetSelectOnLoaded();
+
+ return docViews;
+ };
+
+}
+
type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
@@ -60,18 +182,42 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
private inkKey = "ink";
+ private _childLayoutDisposer?: IReactionDisposer;
+
+ componentDidMount() {
+ this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
+ async (args) => args[1] instanceof Doc &&
+ this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined)));
+ }
+ componentWillUnmount() {
+ this._childLayoutDisposer && this._childLayoutDisposer();
+ }
get parentScaling() {
return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
}
+ ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
+ let bounds = boundsList.reduce((bounds, b) => {
+ var [sptX, sptY] = [b.x, b.y];
+ let [bptX, bptY] = [sptX + NumCast(b.width, 1), sptY + NumCast(b.height, 1)];
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+ return bounds;
+ }
+
@computed get contentBounds() {
- let bounds = this.fitToBox && !this.isAnnotationOverlay ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined;
- return {
+ let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined;
+ let res = {
panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0,
panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0,
scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling
};
+ if (res.scale === 0) res.scale = 1;
+ return res;
}
@computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; }
@@ -85,6 +231,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections
private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections
private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
+ private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1);
private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
@@ -94,6 +241,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
this.props.addDocument(newBox, false);
this.bringToFront(newBox);
+ this.updateClusters();
return true;
}
private selectDocuments = (docs: Doc[]) => {
@@ -113,17 +261,38 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
}
-
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+ }
+ _clusterDistance = 75;
+ boundsOverlap(doc: Doc, doc2: Doc) {
+ var x2 = NumCast(doc2.x) - this._clusterDistance;
+ var y2 = NumCast(doc2.y) - this._clusterDistance;
+ var w2 = NumCast(doc2.width) + this._clusterDistance;
+ var h2 = NumCast(doc2.height) + this._clusterDistance;
+ var x = NumCast(doc.x) - this._clusterDistance;
+ var y = NumCast(doc.y) - this._clusterDistance;
+ var w = NumCast(doc.width) + this._clusterDistance;
+ var h = NumCast(doc.height) + this._clusterDistance;
+ if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) {
+ return true;
+ }
+ return false;
+ }
@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);
if (super.drop(e, de)) {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
- let [xp, yp] = xf.transformPoint(de.x, de.y);
- let x = xp - de.data.xOffset;
- let y = yp - de.data.yOffset;
+ let z = NumCast(de.data.draggedDocuments[0].z);
+ let x = (z ? xpo : xp) - de.data.xOffset;
+ let y = (z ? ypo : yp) - de.data.yOffset;
let dropX = NumCast(de.data.droppedDocuments[0].x);
let dropY = NumCast(de.data.droppedDocuments[0].y);
de.data.droppedDocuments.forEach(d => {
@@ -139,18 +308,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
this.bringToFront(d);
});
+
+ this.updateClusters();
}
}
else if (de.data instanceof DragManager.AnnotationDragData) {
if (de.data.dropDocument) {
let dragDoc = de.data.dropDocument;
- let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
let x = xp - de.data.xOffset;
let y = yp - de.data.yOffset;
let dropX = NumCast(de.data.dropDocument.x);
let dropY = NumCast(de.data.dropDocument.y);
dragDoc.x = x + NumCast(dragDoc.x) - dropX;
dragDoc.y = y + NumCast(dragDoc.y) - dropY;
+ de.data.targetContext = this.props.Document;
+ dragDoc.targetContext = this.props.Document;
this.bringToFront(dragDoc);
}
}
@@ -158,6 +330,87 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
+ tryDragCluster(e: PointerEvent) {
+ let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
+ let cluster = this.childDocs.reduce((cluster, cd) => {
+ let cx = NumCast(cd.x) - this._clusterDistance;
+ let cy = NumCast(cd.y) - this._clusterDistance;
+ let cw = NumCast(cd.width) + 2 * this._clusterDistance;
+ let ch = NumCast(cd.height) + 2 * this._clusterDistance;
+ if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 })) {
+ return NumCast(cd.cluster);
+ }
+ return cluster;
+ }, -1);
+ if (cluster !== -1) {
+ let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
+ this.selectDocuments(eles);
+ let clusterDocs = SelectionManager.SelectedDocuments();
+ SelectionManager.DeselectAll();
+ let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
+ de.moveDocument = this.props.moveDocument;
+ const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
+ const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top);
+ de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+ de.xOffset = xoff;
+ de.yOffset = yoff;
+ DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
+ handlers: { dragComplete: action(emptyFunction) },
+ hideSource: !de.dropAction
+ });
+ return true;
+ }
+
+ return false;
+ }
+ @observable sets: (Doc[])[] = [];
+ @action
+ updateClusters() {
+ this.sets.length = 0;
+ this.childDocs.map(c => {
+ let included = [];
+ for (let i = 0; i < this.sets.length; i++) {
+ for (let member of this.sets[i]) {
+ if (this.boundsOverlap(c, member)) {
+ included.push(i);
+ break;
+ }
+ }
+ }
+ if (included.length === 0) {
+ this.sets.push([c]);
+ } else if (included.length === 1) {
+ this.sets[included[0]].push(c);
+ } else {
+ this.sets[included[0]].push(c);
+ for (let s = 1; s < included.length; s++) {
+ this.sets[included[0]].push(...this.sets[included[s]]);
+ this.sets[included[s]].length = 0;
+ }
+ }
+ });
+ this.sets.map((set, i) => set.map(member => member.cluster = i));
+ }
+
+ getClusterColor = (doc: Doc) => {
+ if (this.props.Document.useClusters) {
+ let cluster = NumCast(doc.cluster);
+ if (this.sets.length <= cluster) {
+ setTimeout(() => this.updateClusters(), 0);
+ return;
+ }
+ let set = this.sets.length > cluster ? this.sets[cluster] : undefined;
+ let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+ let clusterColor = colors[cluster % colors.length];
+ set && set.filter(s => !s.isBackground).map(s =>
+ s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
+ set && set.filter(s => s.isBackground).map(s =>
+ s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
+ return clusterColor;
+ }
+ return "";
+ }
+
@action
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
@@ -178,6 +431,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble) {
+ if (this.props.Document.useClusters && 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;
+ }
let x = this.Document.panX || 0;
let y = this.Document.panY || 0;
let docs = this.childDocs || [];
@@ -209,10 +469,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this._pheight / this.zoomScaling());
let panelwidth = panelDim[0];
let panelheight = panelDim[1];
- // if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
- // if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
- // if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
- // if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
+ if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
+ if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
+ if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
+ if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
}
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
@@ -225,12 +485,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
if (BoolCast(this.props.Document.lockedPosition)) return;
- // if (!this.props.active()) {
- // return;
- // }
- if (this.props.Document.type === "pdf") {
+ if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
+ e.stopPropagation();
return;
}
+
let childSelected = this.childDocs.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
@@ -239,21 +498,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return;
}
e.stopPropagation();
- const coefficient = 1000;
-
- if (e.ctrlKey) {
- let deltaScale = (1 - (e.deltaY / coefficient));
- let nw = this.nativeWidth * deltaScale;
- let nh = this.nativeHeight * deltaScale;
- if (nw && nh) {
- this.props.Document.nativeWidth = nw;
- this.props.Document.nativeHeight = nh;
- }
- e.stopPropagation();
- e.preventDefault();
- } else {
- // if (modes[e.deltaMode] === 'pixels') coefficient = 50;
- // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height??
+
+ // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults. need more logic to fix.
+ // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) {
+ // let deltaScale = (1 - (e.deltaY / coefficient));
+ // let nw = this.nativeWidth * deltaScale;
+ // let nh = this.nativeHeight * deltaScale;
+ // if (nw && nh) {
+ // this.props.Document.nativeWidth = nw;
+ // this.props.Document.nativeHeight = nh;
+ // }
+ // e.preventDefault();
+ // }
+ // else
+ {
let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
deltaScale = 1 / this.zoomScaling();
@@ -265,23 +523,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
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);
- e.stopPropagation();
+ e.preventDefault();
}
}
@action
setPan(panX: number, panY: number) {
- if (BoolCast(this.props.Document.lockedPosition)) return;
- this.props.Document.panTransformType = "None";
- var scale = this.getLocalTransform().inverse().Scale;
- const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
- const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
- this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.props.Document.panY = this.isAnnotationOverlay && StrCast(this.props.Document.backgroundLayout).indexOf("PDFBox") === -1 ? newPanY : panY;
- // this.props.Document.panX = panX;
- // this.props.Document.panY = panY;
- if (this.props.Document.scrollY) {
- this.props.Document.scrollY = panY - scale * this.props.Document[HeightSym]();
+ if (!BoolCast(this.props.Document.lockedPosition)) {
+ this.props.Document.panTransformType = "None";
+ var scale = this.getLocalTransform().inverse().Scale;
+ const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
+ const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.props.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
+ this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
}
}
@@ -294,7 +548,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
onDragOver = (): void => {
}
- bringToFront = (doc: Doc) => {
+ bringToFront = (doc: Doc, sendToBack?: boolean) => {
+ if (sendToBack || doc.isBackground) {
+ doc.zIndex = 0;
+ return;
+ }
const docs = this.childDocs;
docs.slice().sort((doc1, doc2) => {
if (doc1 === doc) return 1;
@@ -361,16 +619,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.Document.scale = scale;
}
- getScale = () => {
- if (this.Document.scale) {
- return this.Document.scale;
- }
- return 1;
- }
-
+ getScale = () => this.Document.scale ? this.Document.scale : 1;
getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
- let self = this;
let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, childDocLayout);
return {
DataDoc: pair.data,
@@ -378,7 +629,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
- ScreenToLocalTransform: this.getTransform,
+ onClick: this.props.onClick,
+ ScreenToLocalTransform: pair.layout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
selectOnLoad: pair.layout[Id] === this._selectOnLoaded,
PanelWidth: pair.layout[WidthSym],
@@ -386,6 +638,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
+ backgroundColor: this.getClusterColor,
parentActive: this.props.active,
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
@@ -401,6 +654,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
+ onClick: this.props.onClick,
ScreenToLocalTransform: this.getTransform,
renderDepth: this.props.renderDepth,
selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
@@ -409,6 +663,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
+ backgroundColor: returnEmptyString,
parentActive: this.props.active,
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
@@ -418,125 +673,169 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
};
}
- getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, width?: number, height?: number, state?: any } {
+ getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, state?: any } {
const result = script.script.run(params);
if (!result.success) {
return {};
}
- return result.result === undefined ? {} : result.result;
+ let doc = params.doc;
+ return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result;
+ }
+
+ viewDefsToJSX = (views: any[]) => {
+ let elements: ViewDefResult[] = [];
+ if (Array.isArray(views)) {
+ elements = views.reduce<typeof elements>((prev, ele) => {
+ const jsx = this.viewDefToJSX(ele);
+ jsx && prev.push(jsx);
+ return prev;
+ }, elements);
+ }
+ return elements;
}
- private viewDefToJSX(viewDef: any): JSX.Element | undefined {
+ private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
if (viewDef.type === "text") {
const text = Cast(viewDef.text, "string");
const x = Cast(viewDef.x, "number");
const y = Cast(viewDef.y, "number");
+ const z = Cast(viewDef.z, "number");
const width = Cast(viewDef.width, "number");
const height = Cast(viewDef.height, "number");
const fontSize = Cast(viewDef.fontSize, "number");
- if ([text, x, y].some(val => val === undefined)) {
+ if ([text, x, y, width, height].some(val => val === undefined)) {
return undefined;
}
- return <div className="collectionFreeform-customText" style={{
- transform: `translate(${x}px, ${y}px)`,
- width, height, fontSize
- }}>{text}</div>;
+ return {
+ ele: <div className="collectionFreeform-customText" style={{
+ transform: `translate(${x}px, ${y}px)`,
+ width, height, fontSize
+ }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
+ };
}
}
@computed.struct
- get views() {
+ get elements() {
+ if (this.Document.usePivotLayout) return PivotView.elements(this);
let curPage = FieldValue(this.Document.curPage, -1);
const initScript = this.Document.arrangeInit;
const script = this.Document.arrangeScript;
let state: any = undefined;
const docs = this.childDocs;
- let elements: JSX.Element[] = [];
+ let elements: ViewDefResult[] = [];
if (initScript) {
const initResult = initScript.script.run({ docs, collection: this.Document });
if (initResult.success) {
const result = initResult.result;
const { state: scriptState, views } = result;
state = scriptState;
- if (Array.isArray(views)) {
- elements = views.reduce<JSX.Element[]>((prev, ele) => {
- const jsx = this.viewDefToJSX(ele);
- jsx && prev.push(jsx);
- return prev;
- }, elements);
- }
+ elements = this.viewDefsToJSX(views);
}
}
- let docviews = docs.reduce((prev, doc) => {
- if (!(doc instanceof Doc)) return prev;
+ let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => {
var page = NumCast(doc.page, -1);
if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) {
let minim = BoolCast(doc.isMinimized);
if (minim === undefined || !minim) {
- const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {};
+ const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) :
+ { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") };
state = pos.state === undefined ? state : pos.state;
- prev.push(<CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />);
+ prev.push({
+ ele: <CollectionFreeFormDocumentView key={doc[Id]}
+ x={script ? pos.x : undefined} y={script ? pos.y : undefined}
+ width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(doc)} />,
+ bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined
+ });
}
}
return prev;
}, elements);
- setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
+ this.resetSelectOnLoaded();
return docviews;
}
+ resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
+
+ @computed.struct
+ get views() {
+ let source = this.elements;
+ return source.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);
+ }
+
+
@action
onCursorMove = (e: React.PointerEvent) => {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- onContextMenu = () => {
+ fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox;
+
+ arrangeContents = async () => {
+ const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
+ UndoManager.RunInBatch(() => {
+ if (docs) {
+ let startX = this.Document.panX || 0;
+ let x = startX;
+ 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)));
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ x += width + 20;
+ if (++i === 6) {
+ i = 0;
+ x = startX;
+ y += height + 20;
+ }
+ }
+ }
+ }, "arrange contents");
+ }
+
+ analyzeStrokes = async () => {
+ let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField);
+ if (!data) {
+ return;
+ }
+ let relevantKeys = ["inkAnalysis", "handwriting"];
+ CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData);
+ }
+
+ onContextMenu = (e: React.MouseEvent) => {
let layoutItems: ContextMenuProps[] = [];
+ layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: this.fitToContainer, icon: !this.fitToBox ? "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.fitToBox ? "Unset" : "Set"} Fit To Container`,
- event: undoBatch(async () => this.props.Document.fitToBox = !this.fitToBox),
- icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt"
+ description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
+ event: async () => {
+ Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes
+ Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
+ this.props.Document.useClusters = !this.props.Document.useClusters;
+ },
+ icon: !this.props.Document.useClusters ? "braille" : "braille"
});
layoutItems.push({
- description: "Arrange contents in grid",
- icon: "table",
- event: async () => {
- const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
- UndoManager.RunInBatch(() => {
- if (docs) {
- let startX = this.Document.panX || 0;
- let x = startX;
- 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)));
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- x += width + 20;
- if (++i === 6) {
- i = 0;
- x = startX;
- y += height + 20;
- }
- }
- }
- }, "arrange contents");
- }
+ description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`,
+ event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground,
+ icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
});
+ layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
- ContextMenu.Instance.addItem({
- description: "Analyze Strokes", event: async () => {
- let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField);
- if (!data) {
- return;
- }
- let relevantKeys = ["inkAnalysis", "handwriting"];
- CognitiveServices.Inking.Manager.analyzer(this.fieldExtensionDoc, relevantKeys, data.inkData);
- }, icon: "paint-brush"
- });
+
+ let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
+ let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
+ analyzers.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+ !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" });
}
@@ -544,6 +843,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
...this.views
]
+ private overlayChildViews = () => {
+ return [...this.overlayViews];
+ }
public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
return () => {
@@ -592,6 +894,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
+ {this.overlayChildViews()}
<CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 1c767e012..aad26efa0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -135,7 +135,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
doc.width = 200;
docList.push(doc);
}
- let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ 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 });
this.props.addDocument(newCol, false);
}
@@ -226,15 +226,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
get ink() {
- let container = this.props.container.Document;
+ let container = this.props.container.props.Document;
let containerKey = this.props.container.props.fieldKey;
- return Cast(container[containerKey + "_ink"], InkField);
+ let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
+ return Cast(extensionDoc.ink, InkField);
}
set ink(value: InkField | undefined) {
- let container = Doc.GetProto(this.props.container.Document);
+ let container = Doc.GetProto(this.props.container.props.Document);
let containerKey = this.props.container.props.fieldKey;
- container[containerKey + "_ink"] = value;
+ let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
+ extensionDoc.ink = value;
}
@undoBatch
@@ -247,7 +249,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._commandExecuted = true;
e.stopPropagation();
(e as any).propagationIsStopped = true;
- this.marqueeSelect().map(d => this.props.removeDocument(d));
+ this.marqueeSelect(false).map(d => this.props.removeDocument(d));
if (this.ink) {
this.marqueeInkDelete(this.ink.inkData);
}
@@ -261,7 +263,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.preventDefault();
(e as any).propagationIsStopped = true;
let bounds = this.Bounds;
- let selected = this.marqueeSelect();
+ let selected = this.marqueeSelect(false);
if (e.key === "c") {
selected.map(d => {
this.props.removeDocument(d);
@@ -278,11 +280,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
panX: 0,
panY: 0,
backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
+ defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
width: bounds.width,
height: bounds.height,
title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
});
- newCollection.data_ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
+ let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
+ dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
this.marqueeInkDelete(inkData);
if (e.key === "s") {
@@ -293,15 +297,16 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
d.page = -1;
return d;
});
+ newCollection.chromeStatus = "disabled";
let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
newCollection.proto!.summaryDoc = summary;
selected = [newCollection];
newCollection.x = bounds.left + bounds.width;
summary.proto!.subBulletDocs = new List<Doc>(selected);
- //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
summary.templates = new List<string>([Templates.Bullet.Layout]);
- let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
+ 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;
this.props.addLiveTextDocument(container);
// });
} else if (e.key === "S") {
@@ -312,6 +317,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
d.page = -1;
return d;
});
+ newCollection.chromeStatus = "disabled";
let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
newCollection.proto!.summaryDoc = summary;
selected = [newCollection];
@@ -319,6 +325,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
//this.props.addDocument(newCollection, false);
summary.proto!.summarizedDocs = new List<Doc>(selected);
summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ summary.autoHeight = true;
this.props.addLiveTextDocument(summary);
}
@@ -363,19 +370,29 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
- marqueeSelect() {
+ marqueeSelect(selectBackgrounds: boolean = true) {
let selRect = this.Bounds;
let selection: Doc[] = [];
this.props.activeDocuments().filter(doc => !doc.isBackground).map(doc => {
- var z = NumCast(doc.zoomBasis, 1);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width) / z;
- var h = NumCast(doc.height) / z;
+ var w = NumCast(doc.width);
+ var h = NumCast(doc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
});
+ if (!selection.length && selectBackgrounds) {
+ this.props.activeDocuments().map(doc => {
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width);
+ var h = NumCast(doc.height);
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
+ selection.push(doc);
+ }
+ });
+ }
return selection;
}