From 088a16c9589e640e814d9120fecc0003ab9d594e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 18 Sep 2019 09:48:15 -0400
Subject: cleaned up some CompileScript code

---
 src/client/views/collections/CollectionViewChromes.tsx | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

(limited to 'src/client/views/collections/CollectionViewChromes.tsx')

diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index c897af17e..7510b86a0 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -215,14 +215,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
             }
         }
         let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
-            `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
-            `return (${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
-            "return true";
+            `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
+            `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
+            "true";
 
-        let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false });
-        if (compiled.compiled) {
-            this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
-        }
+        this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name });
     }
 
     @action
@@ -283,11 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
     @action.bound
     clearFilter = () => {
-        let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false });
-        if (compiled.compiled) {
-            this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
-        }
-
+        this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name });
         this._keyRestrictions = [];
         this.addKeyRestrictions([]);
     }
-- 
cgit v1.2.3-70-g09d2


From b69106994f5450e2ac2c515f28dacceb2e13b78b Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 25 Sep 2019 00:16:55 -0400
Subject: refactored collection view to be cleaner and more modular for
 supporting things like PivotView. fixed pivot view selection bug.

---
 src/Utils.ts                                       |  15 +
 src/client/util/DocumentManager.ts                 |   9 +-
 src/client/util/SelectionManager.ts                |  16 -
 .../views/collections/CollectionBaseView.tsx       |   6 +-
 src/client/views/collections/CollectionView.tsx    |   9 +-
 .../views/collections/CollectionViewChromes.tsx    |   3 +-
 .../CollectionFreeFormLayoutEngines.tsx            | 119 +++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 567 ++++++---------------
 .../collections/collectionFreeForm/MarqueeView.tsx |   5 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  12 +-
 src/client/views/nodes/FormattedTextBox.scss       |  20 +-
 src/client/views/nodes/PresBox.tsx                 |   6 +-
 src/new_fields/Doc.ts                              |  13 +
 13 files changed, 340 insertions(+), 460 deletions(-)
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx

(limited to 'src/client/views/collections/CollectionViewChromes.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 65eb3cffd..4fac53c7d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -235,6 +235,21 @@ export function timenow() {
     return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
 }
 
+export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
+    return boundsList.reduce((bounds, b) => {
+        var [sptX, sptY] = [b.x, b.y];
+        let [bptX, bptY] = [sptX + b.width, sptY + b.height];
+        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 });
+}
+export function 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);
+}
+
 export function percent2frac(percent: string) {
     return Number(percent.substr(0, percent.length - 1)) / 100;
 }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index a3c7429b9..5ade2ebb3 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -188,13 +188,8 @@ export class DocumentManager {
 
     @action
     zoomIntoScale = (docDelegate: Doc, scale: number) => {
-        let doc = Doc.GetProto(docDelegate);
-
-        let docView: DocumentView | null;
-        docView = DocumentManager.Instance.getDocumentView(doc);
-        if (docView) {
-            docView.props.zoomToScale(scale);
-        }
+        let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
+        docView && docView.props.zoomToScale(scale);
     }
 
     getScaleOfDocView = (docDelegate: Doc) => {
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4c97a1056..a02a270ee 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -88,20 +88,4 @@ export namespace SelectionManager {
     export function SelectedDocuments(): Array<DocumentView> {
         return manager.SelectedDocuments.slice();
     }
-    export function ViewsSortedHorizontally(): DocumentView[] {
-        let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
-            if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
-            if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
-            return 0;
-        });
-        return sorted;
-    }
-    export function ViewsSortedVertically(): DocumentView[] {
-        let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
-            if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
-            if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
-            return 0;
-        });
-        return sorted;
-    }
 }
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 56d12bd84..0168c466f 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -20,7 +20,8 @@ export enum CollectionViewType {
     Docking,
     Tree,
     Stacking,
-    Masonry
+    Masonry,
+    Pivot,
 }
 
 export namespace CollectionViewType {
@@ -32,7 +33,8 @@ export namespace CollectionViewType {
         ["docking", CollectionViewType.Docking],
         ["tree", CollectionViewType.Tree],
         ["stacking", CollectionViewType.Stacking],
-        ["masonry", CollectionViewType.Masonry]
+        ["masonry", CollectionViewType.Masonry],
+        ["pivot", CollectionViewType.Pivot]
     ]);
 
     export const valueOf = (value: string) => {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 5f4742834..d3072ff1e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -18,6 +18,7 @@ import { CollectionSchemaView } from "./CollectionSchemaView";
 import { CollectionStackingView } from './CollectionStackingView';
 import { CollectionTreeView } from "./CollectionTreeView";
 import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
 export const COLLECTION_BORDER_WIDTH = 2;
 
 library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -59,8 +60,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
             case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
             case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
             case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
+            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
             case CollectionViewType.Freeform:
             default:
+                this.props.Document.freeformLayoutEngine = undefined;
                 return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
         }
         return (null);
@@ -89,7 +92,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
         if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
             let existingVm = ContextMenu.Instance.findByDescription("View Modes...");
             let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
-            subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" });
+            subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
             if (CollectionBaseView.InSafeMode()) {
                 ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
             }
@@ -103,10 +106,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
                 }, icon: "ellipsis-v"
             });
             subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
+            subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" });
             switch (this.props.Document.viewType) {
                 case CollectionViewType.Freeform: {
-                    subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) });
-                    subItems.push({ description: "Pivot", icon: "copy", event: () => this.props.Document.usePivotLayout = true });
+                    subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
                     break;
                 }
             }
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 7510b86a0..47b300efc 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -262,7 +262,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
     @observable private pivotKeyDisplay = this.pivotKey;
     getPivotInput = () => {
-        if (!this.document.usePivotLayout) {
+        if (StrCast(this.document.freeformLayoutEngine) !== "pivot") {
             return (null);
         }
         return (<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -398,6 +398,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
+                            <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option>
                         </select>
                         <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
                             <input className="collectionViewBaseChrome-viewSpecsInput"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
new file mode 100644
index 000000000..21855b168
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -0,0 +1,119 @@
+import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, Cast } 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");
+
+interface PivotData {
+    type: string;
+    text: string;
+    x: number;
+    y: number;
+    width: number;
+    height: number;
+    fontSize: number;
+}
+
+export interface ViewDefBounds {
+    x: number;
+    y: number;
+    z?: number;
+    width: number;
+    height: number;
+    transition?: string;
+}
+
+export interface ViewDefResult {
+    ele: JSX.Element;
+    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();
+    const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
+    const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
+
+    for (const doc of childDocs) {
+        const val = doc[StrCast(pivotDoc.pivotField, "title")];
+        if (val) {
+            !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
+            pivotColumnGroups.get(val)!.push(doc);
+        }
+    }
+
+    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)));
+    const docMap = new Map<Doc, ViewDefBounds>();
+    const groupNames: PivotData[] = [];
+
+    let x = 0;
+    pivotColumnGroups.forEach((val, key) => {
+        let y = 0;
+        let xCount = 0;
+        groupNames.push({
+            type: "text",
+            text: String(key),
+            x,
+            y: pivotAxisWidth + 50,
+            width: pivotAxisWidth * 1.25 * numCols,
+            height: 100,
+            fontSize: NumCast(pivotDoc.pivotFontSize, 10)
+        });
+        for (const doc of val) {
+            docMap.set(doc, {
+                x: x + xCount * pivotAxisWidth * 1.25,
+                y: -y,
+                width: pivotAxisWidth,
+                height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+            });
+            xCount++;
+            if (xCount >= numCols) {
+                xCount = 0;
+                y += pivotAxisWidth * 1.25;
+            }
+        }
+        x += pivotAxisWidth * 1.25 * (numCols + 1);
+    });
+
+    childPairs.map(pair => {
+        let 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)
+        };
+        const pos = docMap.get(pair.layout) || defaultPosition;
+        layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
+    });
+    return { map: layoutPoolData, 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) => {
+            let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox
+            const scriptField = Cast(doc[key], ScriptField);
+            // tslint:disable-next-line: no-unnecessary-callback-wrapper
+            let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
+                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) => {
+                    const script = CompileScript(text, { params, requiredType, typecheck: false });
+                    if (!script.compiled) {
+                        onError(script.errors.map(error => error.messageText).join("\n"));
+                    } else {
+                        doc[key] = new ScriptField(script);
+                        overlayDisposer();
+                    }
+                }} />;
+            overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
+        };
+        addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
+        addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
+    };
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 438529596..af84a1d73 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,22 +1,23 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faEye } from "@fortawesome/free-regular-svg-icons";
 import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, IReactionDisposer, observable, reaction, trace, ObservableMap, untracked } from "mobx";
+import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } 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, PromiseValue, DateCast } from "../../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils";
+import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import { aggregateBounds, emptyFunction, intersectRect, returnEmptyString, returnOne, Utils } from "../../../../Utils";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { DocServer } from "../../../DocServer";
 import { Docs } from "../../../documents/Documents";
 import { DocumentType } from "../../../documents/DocumentTypes";
 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";
@@ -26,20 +27,17 @@ import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingCanvas } from "../../InkingCanvas";
 import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps, documentSchema } from "../../nodes/DocumentView";
+import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
 import { pageSchema } from "../../nodes/ImageBox";
-import { OverlayElementOptions, OverlayView } from "../../OverlayView";
 import PDFMenu from "../../pdf/PDFMenu";
-import { ScriptBox } from "../../ScriptBox";
 import { CollectionSubView } from "../CollectionSubView";
+import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
 import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
 import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
 import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
-import { DocServer } from "../../../DocServer";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard);
 
@@ -51,33 +49,10 @@ export const panZoomSchema = createSchema({
     arrangeInit: ScriptField,
     useClusters: "boolean",
     isRuleProvider: "boolean",
-    fitToBox: "boolean"
+    fitToBox: "boolean",
+    panTransformType: "string"
 });
 
-export interface ViewDefBounds {
-    x: number;
-    y: number;
-    z?: number;
-    width: number;
-    height: number;
-    transition?: string;
-}
-
-interface PivotData {
-    type: string;
-    text: string;
-    x: number;
-    y: number;
-    width: number;
-    height: number;
-    fontSize: number;
-}
-
-export interface ViewDefResult {
-    ele: JSX.Element;
-    bounds?: ViewDefBounds;
-}
-
 type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
 const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
 
@@ -85,49 +60,25 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch
 export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private _lastX: number = 0;
     private _lastY: number = 0;
-    private get _pwidth() { return this.props.PanelWidth(); }
-    private get _pheight() { return this.props.PanelHeight(); }
-    private 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 actualContentBounds() {
-        return this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined;
-    }
-
-    @computed get contentBounds() {
-        let bounds = this.actualContentBounds;
-        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.Document.fitToBox; }
-    @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; }
-    @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; }
-    public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
+    private _clusterDistance: number = 75;
+    @observable _clusterSets: (Doc[])[] = [];
+
+    @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.fitToContent ? 0 : this.Document.nativeWidth || 0; }
+    @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
+    private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
     private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
-    private panX = () => this.contentBounds.panX;
-    private panY = () => this.contentBounds.panY;
-    private zoomScaling = () => this.contentBounds.scale;
-    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 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 zoomScaling = () => (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)
+        / this.parentScaling;
+    private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0;  // shift so pan position is at center of window for non-overlay collections
+    private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 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);
@@ -145,47 +96,31 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         this.addDocument(newBox, false);
     }
     private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
-        this.props.addDocument(newBox, false);
-        this.bringToFront(newBox);
-        this.updateCluster(newBox);
-        return true;
+        let added = this.props.addDocument(newBox, false);
+        added && this.bringToFront(newBox);
+        added && this.updateCluster(newBox);
+        return added;
     }
     private selectDocuments = (docs: Doc[]) => {
         SelectionManager.DeselectAll();
-        docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
-            SelectionManager.SelectDoc(dv!, true));
+        docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
     }
+    public isCurrent(doc: Doc) { return !this.props.Document.isMinimized && (Math.abs(NumCast(doc.page, -1) - NumCast(this.Document.curPage, -1)) < 1.5 || NumCast(doc.page, -1) === -1); }
+
     public getActiveDocuments = () => {
-        const curPage = FieldValue(this.Document.curPage, -1);
-        return this.childLayoutPairs.filter(pair => {
-            var page = NumCast(pair.layout!.page, -1);
-            return page === curPage || page === -1;
-        }).map(pair => pair.layout);
+        return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     }
 
     @computed get fieldExtensionDoc() {
         return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
     }
 
-    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;
+    @action
+    onDrop = (e: React.DragEvent): void => {
+        var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+        super.onDrop(e, { x: pt[0], y: pt[1] });
     }
+
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
@@ -236,18 +171,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return false;
     }
 
-    tryDragCluster(e: PointerEvent) {
-        let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
-        let cluster = this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
+    pickCluster(probe: number[]) {
+        return this.childLayoutPairs.map(pair => pair.layout).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;
+            return !cd.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);
 
@@ -272,36 +207,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
         return false;
     }
-    @observable sets: (Doc[])[] = [];
 
     @undoBatch
-    @action
     updateClusters(useClusters: boolean) {
-        this.Document.useClusters = useClusters;
-        this.sets.length = 0;
-        this.childLayoutPairs.map(pair => pair.layout).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));
+        this.props.Document.useClusters = useClusters;
+        this._clusterSets.length = 0;
+        this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
     }
 
     @undoBatch
@@ -309,28 +220,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     updateCluster(doc: Doc) {
         let childLayouts = this.childLayoutPairs.map(pair => pair.layout);
         if (this.props.Document.useClusters) {
-            this.sets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
+            this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
             let preferredInd = NumCast(doc.cluster);
             doc.cluster = -1;
-            this.sets.map((set, i) => set.map(member => {
-                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && this.boundsOverlap(doc, member)) {
+            this._clusterSets.map((set, i) => set.map(member => {
+                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
                     doc.cluster = i;
                 }
             }));
-            if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
+            if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
                 doc.cluster = preferredInd;
             }
-            this.sets.map((set, i) => {
+            this._clusterSets.map((set, i) => {
                 if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
                     doc.cluster = i;
                 }
             });
             if (doc.cluster === -1) {
-                doc.cluster = this.sets.length;
-                this.sets.push([doc]);
+                doc.cluster = this._clusterSets.length;
+                this._clusterSets.push([doc]);
             } else {
-                for (let i = this.sets.length; i <= doc.cluster; i++) !this.sets[i] && this.sets.push([]);
-                this.sets[doc.cluster].push(doc);
+                for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
+                this._clusterSets[doc.cluster].push(doc);
             }
         }
     }
@@ -339,13 +250,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let clusterColor = "";
         let cluster = NumCast(doc.cluster);
         if (this.Document.useClusters) {
-            if (this.sets.length <= cluster) {
+            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"];
                 clusterColor = colors[cluster % colors.length];
-                let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)) : undefined;
+                let 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));
@@ -354,8 +265,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return clusterColor;
     }
 
+    _hitCluster = false;
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        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 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
             document.removeEventListener("pointermove", this.onPointerMove);
             document.removeEventListener("pointerup", this.onPointerUp);
@@ -374,7 +287,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @action
     onPointerMove = (e: PointerEvent): void => {
         if (!e.cancelBubble) {
-            if (this.props.Document.useClusters && this.tryDragCluster(e)) {
+            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);
@@ -409,8 +322,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 }
 
                 let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
-                let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling() * cscale,
-                    this._pheight / this.zoomScaling() * cscale);
+                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;
@@ -426,7 +339,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
-        if (BoolCast(this.props.Document.lockedPosition)) return;
+        if (this.props.Document.lockedPosition) return;
         if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
             e.stopPropagation();
         }
@@ -449,7 +362,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     setPan(panX: number, panY: number) {
-        if (!BoolCast(this.props.Document.lockedPosition)) {
+        if (!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));
@@ -459,12 +372,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    @action
-    onDrop = (e: React.DragEvent): void => {
-        var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
-        super.onDrop(e, { x: pt[0], y: pt[1] });
-    }
-
     bringToFront = (doc: Doc, sendToBack?: boolean) => {
         if (sendToBack || doc.isBackground) {
             doc.zIndex = 0;
@@ -502,51 +409,34 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
         HistoryUtil.pushState(newState);
 
-        let px = this.Document.panX;
-        let py = this.Document.panY;
-        let s = this.Document.scale;
-        this.setPan(newPanX, newPanY);
+        let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
 
-        this.props.Document.panTransformType = "Ease";
+        this.setPan(newPanX, newPanY);
+        this.Document.panTransformType = "Ease";
         this.props.focus(this.props.Document);
-        if (willZoom) {
-            this.setScaleToZoom(doc, scale);
-        }
-        console.log("Focused " + this.Document.title + " " + s);
+        willZoom && this.setScaleToZoom(doc, scale);
+
         afterFocus && setTimeout(() => {
             if (afterFocus && afterFocus()) {
-                console.log("UnFocused " + this.Document.title + " " + s);
-                this.Document.panX = px;
-                this.Document.panY = py;
-                this.Document.scale = s;
+                this.Document.panX = savedState.px;
+                this.Document.panY = savedState.py;
+                this.Document.scale = savedState.s;
+                this.Document.panTransformType = savedState.pt;
             }
         }, 1000);
     }
 
     setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
-        let p = this.props;
-        let PanelHeight = p.PanelHeight();
-        let panelWidth = p.PanelWidth();
-
-        let docHeight = NumCast(doc.height);
-        let docWidth = NumCast(doc.width);
-        let targetHeight = scale * PanelHeight;
-        let targetWidth = scale * panelWidth;
-
-        let maxScaleX: number = targetWidth / docWidth;
-        let maxScaleY: number = targetHeight / docHeight;
-        let maxApplicableScale = Math.min(maxScaleX, maxScaleY);
-        this.Document.scale = maxApplicableScale;
+        this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height));
     }
 
     zoomToScale = (scale: number) => {
         this.Document.scale = scale;
     }
 
-    getScale = () => this.Document.scale ? this.Document.scale : 1;
+    getScale = () => this.Document.scale || 1;
 
     getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
-        trace();
         return {
             DataDoc: childData,
             Document: childLayout,
@@ -611,215 +501,109 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     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;
+        return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
     }
 
     private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
         if (viewDef.type === "text") {
-            const text = Cast(viewDef.text, "string");
+            const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below
             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, width, height].some(val => val === undefined)) {
-                return undefined;
-            }
-
-            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! }
-            };
+            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)` }}>
+                        {text}
+                    </div>,
+                    bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
+                };
         }
     }
 
     lookupLayout = (doc: Doc, dataDoc?: Doc) => {
         let data: any = undefined;
-        let compute = this.Document.usePivotLayout ? this.doPivotComputation.map : this._doComputation.map;
-        compute.forEach((value: any, key: { layout: Doc, data?: Doc }) => {
+        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;
+        }
+        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 } : undefined;
+        return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition };
     }
 
-    @computed get doPivotComputation() {
-        let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
-        const field = StrCast(this.props.Document.pivotField) || "title";
-        const width = NumCast(this.props.Document.pivotWidth) || 200;
-        const groups = new Map<FieldResult<Field>, Doc[]>();
-
-        for (const doc of this.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(this.props.Document.pivotNumColumns) || Math.ceil(Math.sqrt(minSize));
-        const fontSize = NumCast(this.props.Document.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 = this.viewDefsToJSX(groupNames);
-        let pairs = this.childLayoutPairs;
-        pairs.map((pair, i) => {
-            let minim = BoolCast(pair.layout.isMinimized);
-            if (minim === undefined || !minim) {
-                let defaultPosition = (): ViewDefBounds => {
-                    return {
-                        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),
-                        transition: "transform 1s"
-                    };
-                };
-                const pos = docMap.get(pair.layout) || defaultPosition();
-                layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
-            }
-        });
-        return { map: layoutPoolData, elements: elements };
-    };
-
+    @computed
+    get doPivotLayout() {
+        return computePivotLayout(this.props.Document, this.childDocs,
+            this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
+    }
 
     @computed
-    get _doComputation() {
+    get doFreeformLayout() {
         let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
-        let curPage = FieldValue(this.Document.curPage, -1);
-        const initScript = this.Document.arrangeInit;
-        let state: any = undefined;
-        let pairs = this.childLayoutPairs;
-        let elements: ViewDefResult[] = [];
-        if (initScript) {
-            const initResult = initScript.script.run({ docs: pairs.map(pair => pair.layout), collection: this.Document }, console.log);
-            if (initResult.success) {
-                const result = initResult.result;
-                const { state: scriptState, views } = result;
-                state = scriptState;
-                elements = this.viewDefsToJSX(views);
-            }
-        }
-        pairs.map((pair, i) => {
-            var page = NumCast(pair.layout.page, -1);
-            if (!pair.layout.isMinimized && ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1)) {
-                const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: pairs.map(pair => pair.layout), state });
-                state = pos.state === undefined ? state : pos.state;
-                layoutPoolData.set(pair, pos);
-            }
+        let 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) : [];
+
+        this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
+            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);
         });
         return { map: layoutPoolData, elements: elements };
     }
 
     @computed
-    get doComputation() {
-        let dc = this.Document.usePivotLayout ? this.doPivotComputation : this._doComputation;
-        let curPage = FieldValue(this.Document.curPage, -1);
-        let pairs = this.childLayoutPairs;
-        let docviews = pairs.reduce((prev, pair) => {
-            var page = NumCast(pair.layout.page, -1);
-            if (!pair.layout.isMinimized && ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1)) {
-                prev.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)
-                });
-            }
-            return prev;
-        }, dc.elements);
+    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;
+        }
+        this.childLayoutPairs.filter(pair => 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)
+            }));
 
-        return { map: dc.map, elements: docviews };
-    }
-    @computed.struct
-    get elements() {
-        return this.doComputation.elements;
+        return computedElementData;
     }
 
-    @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);
-    }
+    @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); }
 
     @action
     onCursorMove = (e: React.PointerEvent) => {
         super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     }
 
-    arrangeContents = async () => {
-        const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
+    layoutDocsInGrid = () => {
         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;
-                    }
+            const docs = DocListCast(this.Document[this.props.fieldKey]);
+            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");
@@ -854,10 +638,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             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.fitToBox ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "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: `${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.arrangeContents, icon: "table" });
+        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: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
         layoutItems.push({
@@ -905,53 +689,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
         ...this.views
     ]
-
-    public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
-        return () => {
-            let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
-                let overlayDisposer: () => void = emptyFunction;
-                const script = Cast(doc[key], ScriptField);
-                let originalText: string | undefined = undefined;
-                if (script) originalText = script.script.originalScript;
-                // tslint:disable-next-line: no-unnecessary-callback-wrapper
-                let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
-                    const script = CompileScript(text, {
-                        params,
-                        requiredType,
-                        typecheck: false
-                    });
-                    if (!script.compiled) {
-                        onError(script.errors.map(error => error.messageText).join("\n"));
-                        return;
-                    }
-                    doc[key] = new ScriptField(script);
-                    overlayDisposer();
-                }} />;
-                overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
-            };
-            addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
-            addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
-        };
-    }
-
     render() {
         // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
-        this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x;
-        this.props.Document.fitY = this.actualContentBounds && this.actualContentBounds.y;
-        this.props.Document.fitW = this.actualContentBounds && (this.actualContentBounds.r - this.actualContentBounds.x);
-        this.props.Document.fitH = this.actualContentBounds && (this.actualContentBounds.b - this.actualContentBounds.y);
+        this.props.Document.fitX = this.contentBounds && this.contentBounds.x;
+        this.props.Document.fitY = this.contentBounds && this.contentBounds.y;
+        this.props.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
+        this.props.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
         // if fieldExt 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
         Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey);
-        const easing = () => this.props.Document.panTransformType === "Ease";
         return (
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
                 <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
                     addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
-                    getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+                    getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
-                        easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+                        easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                         <CollectionFreeFormLinksView {...this.props} key="freeformLinks">
                             <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
                                 {this.childViews}
@@ -969,23 +723,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
-    @computed get overlayView() {
-        return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
-            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
-    }
     render() {
-        return this.overlayView;
+        return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />
     }
 }
 
 @observer
 class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
-    @computed get backgroundView() {
-        return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
-            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
-    }
     render() {
-        return this.props.Document.backgroundLayout ? this.backgroundView : (null);
+        return !this.props.Document.backgroundLayout ? (null) :
+            (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+                renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />)
     }
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index bbea4a555..c85c3e55b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -30,6 +30,7 @@ interface MarqueeViewProps {
     removeDocument: (doc: Doc) => boolean;
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
+    isAnnotationOverlay: boolean;
 }
 
 @observer
@@ -297,8 +298,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 y: bounds.top,
                 panX: 0,
                 panY: 0,
-                backgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
-                defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
+                backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+                defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
                 width: bounds.width,
                 height: bounds.height,
                 title: "a nested collection",
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 0d9ace473..b30055071 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -41,7 +41,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
     @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
     @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
-    @computed get scaleToOverridingWidth() { return this.width / FieldValue(this.Document.width, this.width); }
 
     @computed get renderScriptDim() {
         if (this.Document.renderScript) {
@@ -74,7 +73,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     panelHeight = () => this.props.PanelHeight();
     getTransform = (): Transform => this.props.ScreenToLocalTransform()
         .translate(-this.X, -this.Y)
-        .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth)
+        .scale(1 / this.contentScaling())
 
     borderRounding = () => {
         let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
@@ -100,9 +99,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     @observable _animPos: number[] | undefined = undefined;
 
+    finalPanelWidh = () => { return this.dataProvider ? this.dataProvider.width : this.panelWidth(); }
+    finalPanelHeight = () => { return this.dataProvider ? this.dataProvider.height : this.panelHeight(); }
+
     render() {
-        console.log("this.props =" + this.props.dataProvider);
-        trace();
         return (
             <div className="collectionFreeFormDocumentView-container"
                 style={{
@@ -123,8 +123,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     ContentScaling={this.contentScaling}
                     ScreenToLocalTransform={this.getTransform}
                     backgroundColor={this.clusterColorFunc}
-                    PanelWidth={this.panelWidth}
-                    PanelHeight={this.panelHeight}
+                    PanelWidth={this.finalPanelWidh}
+                    PanelHeight={this.finalPanelHeight}
                 />
             </div>
         );
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 435f5c055..45e516015 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -164,13 +164,13 @@ ol { counter-reset: deci1 0;}
 .upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
 .lower-roman-ol {counter-reset: lroman;  p { display: inline }; font-size: 14; }
 .lower-alpha-ol {counter-reset: lalpha;  p { display: inline }; font-size: 10;}
-.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; min-width: 30;}
-.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; min-width: 35}
-.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; min-width: 35}
-.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; min-width: 40}
-.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; min-width: 40}
-.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; min-width: 45}
-.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; min-width: 50}
-.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; min-width: 35 }
-.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; min-width: 50 }
-.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; min-width: 35}
+.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;}
+.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35}
+.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ") "; counter-increment: deci4; display:inline-block; min-width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ") "; counter-increment: deci5; display:inline-block; min-width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ") "; counter-increment: deci6; display:inline-block; min-width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ") "; counter-increment: deci7; display:inline-block; min-width: 50}
+.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 }
+.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 }
+.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35}
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index e376fbddb..5afd85430 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -161,10 +161,8 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
             if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.showButton) {
                 let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
                 let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
-                if (prevScale !== undefined) {
-                    if (prevScale !== curScale) {
-                        DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
-                    }
+                if (prevScale !== undefined && prevScale !== curScale) {
+                    DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
                 }
             }
         }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index b875e7896..1b3c8b0b0 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -13,6 +13,7 @@ import { listSpec } from "./Schema";
 import { ComputedField } from "./ScriptField";
 import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types";
 import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { intersectRect } from "../Utils";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -614,6 +615,18 @@ export namespace Doc {
         }), 0);
     }
 
+    export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
+        var x2 = NumCast(doc2.x) - clusterDistance;
+        var y2 = NumCast(doc2.y) - clusterDistance;
+        var w2 = NumCast(doc2.width) + clusterDistance;
+        var h2 = NumCast(doc2.height) + clusterDistance;
+        var x = NumCast(doc.x) - clusterDistance;
+        var y = NumCast(doc.y) - clusterDistance;
+        var w = NumCast(doc.width) + clusterDistance;
+        var h = NumCast(doc.height) + clusterDistance;
+        return doc.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
+    }
+
     export function isBrushedHighlightedDegree(doc: Doc) {
         if (Doc.IsHighlighted(doc)) {
             return 3;
-- 
cgit v1.2.3-70-g09d2


From 2a393bd745667fa920d105bf69827c75dc687f08 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 1 Oct 2019 01:33:57 -0400
Subject: final updates to presentation view to improve layout. fixes for
 chromeHeight

---
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 .../views/collections/CollectionStackingView.tsx   |  6 ++--
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 src/client/views/collections/CollectionView.tsx    | 19 +++++++----
 .../views/collections/CollectionViewChromes.tsx    |  3 +-
 src/client/views/nodes/PresBox.tsx                 | 10 ++++--
 .../views/presentationview/PresElementBox.scss     | 14 ++++++--
 .../views/presentationview/PresElementBox.tsx      | 38 ++++++++++------------
 8 files changed, 52 insertions(+), 42 deletions(-)

(limited to 'src/client/views/collections/CollectionViewChromes.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index d83530d4f..246546c9e 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -573,7 +573,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         //add this new doc to props.Document
         let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
         if (curPres) {
-            let pinDoc = Docs.Create.PresElementBoxDocument();
+            let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
             Doc.GetProto(pinDoc).target = doc;
             Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()');
             const data = Cast(curPres.data, listSpec(Doc));
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index cb272ef1c..c7060f110 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -203,9 +203,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
-        // bcz: this is here for now to account for the presentation stacking view being offset from the document top in PresBox's.  Should generalize this somehow.
-        let offsethack = Number(this._masonryGridRef && this._masonryGridRef.parentElement!.parentElement!.offsetTop);
-        let where = [de.x, de.y - offsethack];
+        let where = [de.x, de.y];
         let targInd = -1;
         let plusOne = false;
         if (de.data instanceof DragManager.DocumentDragData) {
@@ -285,7 +283,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
         let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
         return this.props.ScreenToLocalTransform().
-            translate(offset[0], offset[1]).
+            translate(offset[0], offset[1] + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)).
             scale(NumCast(doc.width, 1) / this.columnWidth);
     }
     masonryChildren(docs: Doc[]) {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index a133e2c51..6ce5d8e20 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -81,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> {
     private _header?: React.RefObject<HTMLDivElement> = React.createRef();
     private _treedropDisposer?: DragManager.DragDropDisposer;
     private _dref = React.createRef<HTMLDivElement>();
-    get defaultExpandedView() { return this.childDocs ? this.fieldKey : "fields"; }
+    get defaultExpandedView() { return this.childDocs ? this.fieldKey : this.props.document.defaultExpandedView ? StrCast(this.props.document.defaultExpandedView) : ""; }
     @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
     @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; }
     set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = c; }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6eb444dde..534246326 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -50,20 +50,25 @@ export class CollectionView extends React.Component<FieldViewProps> {
         this._reactionDisposer && this._reactionDisposer();
     }
 
+    // bcz: Argh?  What's the height of the collection chomes??  
+    chromeHeight = () => {
+        return (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+    }
+
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         let props = { ...this.props, ...renderProps };
         switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) {
-            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
             // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
-            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} CollectionView={this} />);
-            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
-            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
-            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
-            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
+            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
+            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
+            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
             case CollectionViewType.Freeform:
             default:
                 this.props.Document.freeformLayoutEngine = undefined;
-                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
         }
         return (null);
     }
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 47b300efc..23001f0f5 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -10,7 +10,6 @@ import { ScriptField } from "../../../new_fields/ScriptField";
 import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { Utils, emptyFunction } from "../../../Utils";
 import { DragManager } from "../../util/DragManager";
-import { CompileScript } from "../../util/Scripting";
 import { undoBatch } from "../../util/UndoManager";
 import { EditableView } from "../EditableView";
 import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
@@ -375,7 +374,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     render() {
         let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
         return (
-            <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0 }}>
+            <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
                 <div className="collectionViewChrome">
                     <div className="collectionViewBaseChrome">
                         <button className="collectionViewBaseChrome-collapse"
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index ab777d534..66608fb8d 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -42,7 +42,7 @@ export class PresBox extends React.Component<FieldViewProps> {
             if (value) {
                 value.forEach((item, i) => {
                     if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
-                        let pinDoc = Docs.Create.PresElementBoxDocument();
+                        let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
                         Doc.GetProto(pinDoc).target = item;
                         Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()');
                         value.splice(i, 1, pinDoc);
@@ -342,7 +342,8 @@ export class PresBox extends React.Component<FieldViewProps> {
         docList.forEach((doc: Doc) => {
             doc.presBox = this.props.Document;
             doc.presBoxKey = this.props.fieldKey;
-            doc.height = hgt;
+            doc.collapsedHeight = hgt;
+            doc.height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)")
             let curScale = NumCast(doc.viewScale, null);
             if (curScale === undefined) {
                 doc.viewScale = 1;
@@ -356,6 +357,9 @@ export class PresBox extends React.Component<FieldViewProps> {
         index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc));
     }
 
+    getTransform = () => {
+        return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight
+    }
     render() {
         this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType));
         return (
@@ -370,7 +374,7 @@ export class PresBox extends React.Component<FieldViewProps> {
                 </div>
                 {this.props.Document.minimizedView ? (null) :
                     <div className="presBox-listCont" >
-                        <CollectionView {...this.props} focus={this.selectElement} />
+                        <CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
                     </div>
                 }
             </div>
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
index 51f2d2ab8..34c170be2 100644
--- a/src/client/views/presentationview/PresElementBox.scss
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -1,10 +1,12 @@
 .presElementBox-item {
     padding: 10px;
     display: inline-block;
+    background-color: #eeeeee;
     pointer-events: all;
     width: 100%;
     outline-color: maroon;
     outline-style: dashed;
+    border-radius: 12px;
     -webkit-touch-callout: none;
     -webkit-user-select: none;
     -khtml-user-select: none;
@@ -40,9 +42,10 @@
     box-shadow: black 2px 2px 5px;
 }
 
-
-.presElementBox-icon {
+.presElementBox-closeIcon {
     float: right;
+    border-radius: 20px;
+    transform:scale(0.7);
 }
 
 .presElementBox-interaction {
@@ -57,12 +60,17 @@
 
 .presElementBox-name {
     font-size: 15px;
+    position: absolute;
     display: inline-block;
+    width: calc(100% - 45px);
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: pre;
 }
 
 .presElementBox-embedded {
     position: relative;
-    margin-top: 15;
+    margin-top: 30;
 }
 
 .presElementBox-embeddedMask {
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 0e3d8bb7f..fb9474cde 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -31,7 +31,6 @@ library.add(faArrowDown);
 export class PresElementBox extends React.Component<FieldViewProps> {
 
     public static LayoutString() { return FieldView.LayoutString(PresElementBox); }
-    private _embedHeight = 100;
 
     @computed get myIndex() { return DocListCast(this.presentationDoc[this.presentationFieldKey]).indexOf(this.props.Document); }
     @computed get presentationDoc() { return this.props.Document.presBox as Doc; }
@@ -43,9 +42,9 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @computed get fadeButton() { return BoolCast(this.props.Document.fadeButton); }
     @computed get hideAfterButton() { return BoolCast(this.props.Document.hideAfterButton); }
     @computed get groupButton() { return BoolCast(this.props.Document.groupButton); }
-    @computed get embedInline() { return BoolCast(this.props.Document.embedOpen); }
+    @computed get embedOpen() { return BoolCast(this.props.Document.embedOpen); }
 
-    set embedInline(value: boolean) { this.props.Document.embedOpen = value; }
+    set embedOpen(value: boolean) { this.props.Document.embedOpen = value; }
     set showButton(val: boolean) { this.props.Document.showButton = val; }
     set navButton(val: boolean) { this.props.Document.navButton = val; }
     set hideTillShownButton(val: boolean) { this.props.Document.hideTillShownButton = val; }
@@ -162,7 +161,7 @@ export class PresElementBox extends React.Component<FieldViewProps> {
      * presentation element.
      */
     renderEmbeddedInline = () => {
-        if (!this.embedInline || !(this.props.Document.target instanceof Doc)) {
+        if (!this.embedOpen || !(this.props.Document.target instanceof Doc)) {
             return (null);
         }
 
@@ -171,7 +170,7 @@ export class PresElementBox extends React.Component<FieldViewProps> {
         let scale = () => 175 / NumCast(this.props.Document.nativeWidth, 175);
         return (
             <div className="presElementBox-embedded" style={{
-                height: propDocHeight === 0 ? this._embedHeight : propDocHeight * scale(),
+                height: propDocHeight === 0 ? NumCast(this.props.Document.height) - NumCast(this.props.Document.collapsedHeight) : propDocHeight * scale(),
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <DocumentView
@@ -207,31 +206,28 @@ export class PresElementBox extends React.Component<FieldViewProps> {
 
         let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
         let className = "presElementBox-item" + (this.currentIndex === this.myIndex ? " presElementBox-selected" : "");
+        let pbi = "presElementBox-interaction";
         return (
             <div className={className} key={p.Document[Id] + this.myIndex}
-                style={{ outlineWidth: Doc.IsBrushed(p.Document) ? `1px` : "0px", }}
-                onClick={e => p.focus(p.Document)}>
+                style={{ outlineWidth: Doc.IsBrushed(p.Document.target as Doc) ? `1px` : "0px", }}
+                onClick={e => { p.focus(p.Document); e.stopPropagation(); }}>
                 {treecontainer ? (null) : <>
                     <strong className="presElementBox-name">
                         {`${this.myIndex + 1}. ${p.Document.title}`}
                     </strong>
-                    <button className="presElementBox-icon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button>
+                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button>
                     <br />
                 </>
                 }
-                <button title="Zoom" className={"presElementBox-interaction" + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
-                <button title="Navigate" className={"presElementBox-interaction" + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
-                <button title="Hide Til Presented" className={"presElementBox-interaction" + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
-                <button title="Fade After Presented" className={"presElementBox-interaction" + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Hide After Presented" className={"presElementBox-interaction" + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Group With Up" className={"presElementBox-interaction" + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
-                <button title="Expand Inline" className={"presElementBox-interaction" + (this.embedInline ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => {
-                    this.embedInline = !this.embedInline;
-                    this.props.Document.height = NumCast(this.props.Document.height) + (this.embedInline ? 1 : -1) * this._embedHeight;
-                    e.stopPropagation();
-                })}><FontAwesomeIcon icon={"arrow-down"} /></button>
-
-                <br />
+                <button title="Zoom" className={pbi + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+                <button title="Navigate" className={pbi + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+                <button title="Hide Before" className={pbi + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+                <button title="Fade After" className={pbi + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+                <button title="Hide After" className={pbi + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+                <button title="Group With Up" className={pbi + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}><FontAwesomeIcon icon={"arrow-up"} /></button>
+                <button title="Expand Inline" className={pbi + (this.embedOpen ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.embedOpen = !this.embedOpen; })}><FontAwesomeIcon icon={"arrow-down"} /></button>
+
+                <br style={{ lineHeight: 0.1 }} />
                 {this.renderEmbeddedInline()}
             </div>
         );
-- 
cgit v1.2.3-70-g09d2

