From 96de4bf18d5721d2b7957d7adb8ad3393462c4d1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 31 Jan 2020 11:04:58 -0500
Subject: lint

---
 src/client/views/nodes/ImageBox.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 33c694c6e..382ca526d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -67,6 +67,7 @@ const uploadIcons = {
 
 @observer
 export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+    protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
     private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
     private _dropDisposer?: DragManager.DragDropDisposer;
-- 
cgit v1.2.3-70-g09d2


From 693665069c170983bd76336b804ec73870642fe3 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 1 Feb 2020 16:25:10 -0500
Subject: simplified Presentations a bit by using templates

---
 .../views/collections/CollectionDockingView.tsx    | 15 ++--
 src/client/views/nodes/DocumentContentsView.tsx    |  7 +-
 src/client/views/nodes/ImageBox.tsx                |  6 +-
 src/client/views/nodes/PresBox.tsx                 | 54 +++++++-------
 .../views/presentationview/PresElementBox.tsx      | 85 ++++++++++++----------
 src/new_fields/Doc.ts                              | 16 ++--
 6 files changed, 97 insertions(+), 86 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 159a1cded..e691788ad 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -35,6 +35,7 @@ import { ComputedField } from '../../../new_fields/ScriptField';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { TraceMobx } from '../../../new_fields/util';
 import { Scripting } from '../../util/Scripting';
+import { PresElementBox } from '../presentationview/PresElementBox';
 library.add(faFile);
 const _global = (window /* browser */ || global /* node */) as any;
 
@@ -326,7 +327,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     componentDidMount: () => void = () => {
         if (this._containerRef.current) {
             this.reactionDisposer = reaction(
-                () => StrCast(this.props.Document.dockingConfig),
+                () => this.props.Document.dockingConfig,
                 () => {
                     if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
                         // Because this is in a set timeout, if this component unmounts right after mounting,
@@ -643,15 +644,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         //add this new doc to props.Document
         const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
         if (curPres) {
-            const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5 });
-            Doc.GetProto(pinDoc).presentationTargetDoc = doc;
-            Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title?.toString()');
-            const data = Cast(curPres.data, listSpec(Doc));
-            if (data) {
-                data.push(pinDoc);
-            } else {
-                curPres.data = new List([pinDoc]);
-            }
+            const pinDoc = Doc.MakeAlias(doc);
+            pinDoc.presentationTargetDoc = doc;
+            Doc.AddDocToList(curPres, "data", pinDoc);
             if (!DocumentManager.Instance.getDocumentView(curPres)) {
                 this.addDocTab(curPres, undefined, "onRight");
             }
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 0b01e6471..3b9015994 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -81,6 +81,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc;
     }
     get layoutDoc() {
+        if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") {
+            // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), 
+            // then we render the layout document as a template and use this document as the data context for the template layout.
+            return Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document);
+        }
         return Doc.Layout(this.props.Document);
     }
 
@@ -95,7 +100,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
 
     render() {
         TraceMobx();
-        return (this.props.renderDepth > 7 || !this.layout) ? (null) :
+        return (this.props.renderDepth > 7 || !this.layout || !this.layoutDoc) ? (null) :
             <ObserverJsxParser
                 blacklistedAttrs={[]}
                 components={{
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 382ca526d..66b0e5a15 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -259,8 +259,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 .catch((err: any) => console.log(err));
         } else if (this.Document._nativeHeight !== cachedNativeSize.width || this.Document._nativeWidth !== cachedNativeSize.height) {
             !(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => {
-                this.Document._nativeWidth = cachedNativeSize.width;
-                this.Document._nativeHeight = cachedNativeSize.height;
+                if (!(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc)) {
+                    this.Document._nativeWidth = cachedNativeSize.width;
+                    this.Document._nativeHeight = cachedNativeSize.height;
+                }
             }, 0);
         }
     }
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 8d53f45a6..c661b7f4e 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -2,23 +2,22 @@ import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, reaction, IReactionDisposer } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
 import { listSpec } from "../../../new_fields/Schema";
+import { ComputedField } from "../../../new_fields/ScriptField";
 import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Docs } from "../../documents/Documents";
 import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
-import { CollectionViewType } from "../collections/CollectionView";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionView } from "../collections/CollectionView";
+import { CollectionView, CollectionViewType } from "../collections/CollectionView";
 import { ContextMenu } from "../ContextMenu";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./PresBox.scss";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { Docs } from "../../documents/Documents";
-import { ComputedField } from "../../../new_fields/ScriptField";
+import { presSchema } from "../presentationview/PresElementBox";
 
 library.add(faArrowLeft);
 library.add(faArrowRight);
@@ -32,28 +31,18 @@ library.add(faEdit);
 @observer
 export class PresBox extends React.Component<FieldViewProps> {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
-    _docListChangedReaction: IReactionDisposer | undefined;
     componentDidMount() {
-        this._docListChangedReaction = reaction(() => {
-            const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
-            return value ? value.slice() : value;
-        }, () => {
-            const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
-            if (value) {
-                value.forEach((item, i) => {
-                    if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
-                        const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5 });
-                        Doc.GetProto(pinDoc).presentationTargetDoc = item;
-                        Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('this.presentationTargetDoc?.title?.toString()');
-                        value.splice(i, 1, pinDoc);
-                    }
-                });
-            }
-        });
-    }
-
-    componentWillUnmount() {
-        this._docListChangedReaction && this._docListChangedReaction();
+        const userDoc = CurrentUserUtils.UserDocument;
+        let presTemp = Cast(userDoc.presentationTemplate, Doc);
+        if (presTemp instanceof Promise) {
+            presTemp.then(presTemp => this.props.Document.childLayout = presTemp);
+        }
+        else if (presTemp === undefined) {
+            presTemp = userDoc.presentationTemplate = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5, isTemplateDoc: true, isTemplateForField: "data" });
+        }
+        else {
+            this.props.Document.childLayout = presTemp;
+        }
     }
 
     @computed get childDocs() { return DocListCast(this.props.Document[this.props.fieldKey]); }
@@ -282,6 +271,13 @@ export class PresBox extends React.Component<FieldViewProps> {
         }
     }
 
+    addDocument = (doc: Doc) => {
+        const newPinDoc = Doc.MakeAlias(doc);
+        newPinDoc.presentationTargetDoc = doc;
+        return Doc.AddDocToList(this.props.Document, this.props.fieldKey, newPinDoc);
+    }
+
+
     //The function that resets the presentation by removing every action done by it. It also
     //stops the presentaton.
     @action
@@ -342,7 +338,7 @@ export class PresBox extends React.Component<FieldViewProps> {
             doc.presBox = this.props.Document;
             doc.presBoxKey = this.props.fieldKey;
             doc.collapsedHeight = hgt;
-            doc._height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)");
+            doc._nativeWidth = doc._nativeHeight = undefined;
             const curScale = NumCast(doc.viewScale, null);
             if (curScale === undefined) {
                 doc.viewScale = 1;
@@ -373,7 +369,7 @@ export class PresBox extends React.Component<FieldViewProps> {
                 </div>
                 {this.props.Document.inOverlay ? (null) :
                     <div className="presBox-listCont" >
-                        <CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
+                        <CollectionView {...this.props} addDocument={this.addDocument} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
                     </div>
                 }
             </div>
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index dad55e1fd..afc88833e 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -2,7 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core';
 import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
 import { faArrowDown, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed } from "mobx";
+import { action, computed, reaction, IReactionDisposer } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { documentSchema } from '../../../new_fields/documentSchemas';
@@ -14,7 +14,7 @@ import { DocumentType } from "../../documents/DocumentTypes";
 import { Transform } from "../../util/Transform";
 import { CollectionViewType } from '../collections/CollectionView';
 import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
-import { DocComponent } from '../DocComponent';
+import { DocComponent, DocExtendableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import "./PresElementBox.scss";
 import React = require("react");
@@ -46,13 +46,23 @@ const PresDocument = makeInterface(presSchema, documentSchema);
  * It involves some functionality for its buttons and options.
  */
 @observer
-export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(PresDocument) {
+export class PresElementBox extends DocExtendableComponent<FieldViewProps, PresDocument>(PresDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
 
-    @computed get indexInPres() { return DocListCast(this.presentationDoc[this.Document.presBoxKey || ""]).indexOf(this.props.Document); }
-    @computed get presentationDoc() { return Cast(this.Document.presBox, Doc) as Doc; }
-    @computed get targetDoc() { return this.Document.presentationTargetDoc as Doc; }
-    @computed get currentIndex() { return NumCast(this.presentationDoc.selectedDoc); }
+    _heightDisposer: IReactionDisposer | undefined;
+    @computed get indexInPres() { return this.originalLayout?.presBoxKey ? DocListCast(this.presentationDoc[StrCast(this.originalLayout?.presBoxKey)]).indexOf(this.originalLayout) : 0; }
+    @computed get presentationDoc() { return Cast(this.originalLayout?.presBox, Doc) as Doc; }
+    @computed get originalLayout() { return this.props.Document.expandedTemplate as Doc; }
+    @computed get targetDoc() { return this.originalLayout?.presentationTargetDoc as Doc; }
+    @computed get currentIndex() { return NumCast(this.presentationDoc?.selectedDoc); }
+
+    componentDidMount() {
+        this._heightDisposer = reaction(() => [this.originalLayout.embedOpen, this.originalLayout.collapsedHeight],
+            params => this.originalLayout._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+    }
+    componentWillUnmount() {
+        this._heightDisposer?.();
+    }
 
     /**
      * The function that is called on click to turn Hiding document till press option on/off.
@@ -61,8 +71,8 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     @action
     onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.Document.hideTillShownButton = !this.Document.hideTillShownButton;
-        if (!this.Document.hideTillShownButton) {
+        this.originalLayout.hideTillShownButton = !this.originalLayout.hideTillShownButton;
+        if (!this.originalLayout.hideTillShownButton) {
             if (this.indexInPres >= this.currentIndex && this.targetDoc) {
                 this.targetDoc.opacity = 1;
             }
@@ -81,13 +91,13 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     @action
     onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.Document.hideAfterButton = !this.Document.hideAfterButton;
-        if (!this.Document.hideAfterButton) {
+        this.originalLayout.hideAfterButton = !this.originalLayout.hideAfterButton;
+        if (!this.originalLayout.hideAfterButton) {
             if (this.indexInPres <= this.currentIndex && this.targetDoc) {
                 this.targetDoc.opacity = 1;
             }
         } else {
-            if (this.Document.fadeButton) this.Document.fadeButton = false;
+            if (this.originalLayout.fadeButton) this.originalLayout.fadeButton = false;
             if (this.presentationDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
                 this.targetDoc.opacity = 0;
             }
@@ -102,13 +112,13 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     @action
     onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.Document.fadeButton = !this.Document.fadeButton;
-        if (!this.Document.fadeButton) {
+        this.originalLayout.fadeButton = !this.originalLayout.fadeButton;
+        if (!this.originalLayout.fadeButton) {
             if (this.indexInPres <= this.currentIndex && this.targetDoc) {
                 this.targetDoc.opacity = 1;
             }
         } else {
-            this.Document.hideAfterButton = false;
+            this.originalLayout.hideAfterButton = false;
             if (this.presentationDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
                 this.targetDoc.opacity = 0.5;
             }
@@ -121,11 +131,11 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     @action
     onNavigateDocumentClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.Document.navButton = !this.Document.navButton;
-        if (this.Document.navButton) {
-            this.Document.showButton = false;
+        this.originalLayout.navButton = !this.originalLayout.navButton;
+        if (this.originalLayout.navButton) {
+            this.originalLayout.showButton = false;
             if (this.currentIndex === this.indexInPres) {
-                this.props.focus(this.props.Document);
+                this.props.focus(this.originalLayout);
             }
         }
     }
@@ -137,13 +147,13 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     onZoomDocumentClick = (e: React.MouseEvent) => {
         e.stopPropagation();
 
-        this.Document.showButton = !this.Document.showButton;
-        if (!this.Document.showButton) {
-            this.props.Document.viewScale = 1;
+        this.originalLayout.showButton = !this.originalLayout.showButton;
+        if (!this.originalLayout.showButton) {
+            this.originalLayout.viewScale = 1;
         } else {
-            this.Document.navButton = false;
+            this.originalLayout.navButton = false;
             if (this.currentIndex === this.indexInPres) {
-                this.props.focus(this.props.Document);
+                this.props.focus(this.originalLayout);
             }
         }
     }
@@ -157,7 +167,7 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
      * presentation element.
      */
     renderEmbeddedInline = () => {
-        if (!this.Document.embedOpen || !this.targetDoc) {
+        if (!this.originalLayout.embedOpen || !this.targetDoc) {
             return (null);
         }
 
@@ -170,7 +180,8 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <ContentFittingDocumentView
-                    Document={this.targetDoc}
+                    Document={this.props.Document}
+                    DataDocument={this.props.DataDoc}
                     LibraryPath={emptyPath}
                     fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
                     addDocument={returnFalse}
@@ -195,24 +206,24 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
         const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree;
         const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
         const pbi = "presElementBox-interaction";
-        return (
+        return !this.originalLayout ? (null) : (
             <div className={className} key={this.props.Document[Id] + this.indexInPres}
                 style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
-                onClick={e => { this.props.focus(this.props.Document); e.stopPropagation(); }}>
+                onClick={e => { this.props.focus(this.originalLayout); e.stopPropagation(); }}>
                 {treecontainer ? (null) : <>
                     <strong className="presElementBox-name">
-                        {`${this.indexInPres + 1}. ${this.Document.title}`}
+                        {`${this.indexInPres + 1}. ${this.targetDoc?.title}`}
                     </strong>
-                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.props.Document)}>X</button>
+                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.originalLayout)}>X</button>
                     <br />
                 </>}
-                <button title="Zoom" className={pbi + (this.Document.showButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
-                <button title="Navigate" className={pbi + (this.Document.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
-                <button title="Hide Before" className={pbi + (this.Document.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
-                <button title="Fade After" className={pbi + (this.Document.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Hide After" className={pbi + (this.Document.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Group With Up" className={pbi + (this.Document.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.groupButton = !this.Document.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
-                <button title="Expand Inline" className={pbi + (this.Document.embedOpen ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.embedOpen = !this.Document.embedOpen; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
+                <button title="Zoom" className={pbi + (this.originalLayout.showButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+                <button title="Navigate" className={pbi + (this.originalLayout.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+                <button title="Hide Before" className={pbi + (this.originalLayout.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+                <button title="Fade After" className={pbi + (this.originalLayout.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+                <button title="Hide After" className={pbi + (this.originalLayout.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+                <button title="Group With Up" className={pbi + (this.originalLayout.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.originalLayout.groupButton = !this.originalLayout.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
+                <button title="Expand Inline" className={pbi + (this.originalLayout.embedOpen ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.originalLayout.embedOpen = !this.originalLayout.embedOpen; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
 
                 <br style={{ lineHeight: 0.1 }} />
                 {this.renderEmbeddedInline()}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 5f78636a9..0624d6698 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -445,8 +445,8 @@ export namespace Doc {
     // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties 
     // of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
     //
-    export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
-        if (!WillExpandTemplateLayout(templateLayoutDoc, dataDoc) || !dataDoc) return templateLayoutDoc;
+    export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) {
+        if (!WillExpandTemplateLayout(templateLayoutDoc, targetDoc) || !targetDoc) return templateLayoutDoc;
 
         const templateField = StrCast(templateLayoutDoc.isTemplateForField);  // the field that the template renders
         // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
@@ -455,13 +455,15 @@ export namespace Doc {
         // saves it on the data doc indexed by the template layout's id.
         //
         const expandedLayoutFieldKey = templateField + "-layout[" + templateLayoutDoc[Id] + "]";
-        const expandedTemplateLayout = dataDoc?.[expandedLayoutFieldKey];
+        const expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
         if (expandedTemplateLayout === undefined) {
             setTimeout(() => {
-                if (!dataDoc[expandedLayoutFieldKey]) {
+                if (!targetDoc[expandedLayoutFieldKey]) {
                     const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
-                    dataDoc[expandedLayoutFieldKey] = newLayoutDoc;
-                    newLayoutDoc.resolvedDataDoc = dataDoc;
+                    newLayoutDoc.expandedTemplate = targetDoc;
+                    targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
+                    const dataDoc = Doc.GetProto(targetDoc);
+                    newLayoutDoc.resolvedDataDoc = targetDoc;
                     if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && Cast(templateLayoutDoc[templateField], listSpec(Doc), []).length) {
                         dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc: templateLayoutDoc });
                     }
@@ -474,7 +476,7 @@ export namespace Doc {
     // if the childDoc is a template for a field, then this will return the expanded layout with its data doc.
     // otherwise, it just returns the childDoc
     export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
-        const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc ? undefined : Doc.GetDataDoc(containerDataDoc);
+        const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc ? undefined : containerDataDoc;
         return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
     }
     export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-- 
cgit v1.2.3-70-g09d2


From 8597134b6ada1e57ae08d49e24c00d11f728ba90 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 6 Feb 2020 16:04:04 -0500
Subject: restored isBackground for documents that don't want to respond to
 anything.

---
 src/client/util/DragManager.ts                                       | 4 ++--
 .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx  | 1 +
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx            | 1 +
 src/client/views/nodes/ImageBox.scss                                 | 5 ++---
 src/client/views/nodes/ImageBox.tsx                                  | 3 ++-
 5 files changed, 8 insertions(+), 6 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index b0dd90947..e572f0fcb 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -15,7 +15,7 @@ import { listSpec } from "../../new_fields/Schema";
 import { Scripting } from "./Scripting";
 import { convertDropDataToButtons } from "./DropConverter";
 
-export type dropActionType = "alias" | "copy" | "move" | undefined;
+export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(
     _reference: React.RefObject<HTMLElement>,
     docFunc: () => Doc | Promise<Doc> | undefined,
@@ -198,7 +198,7 @@ export namespace DragManager {
             );
             e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) =>
                 Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => {
-                    drop[prop] = "move";
+                    drop[prop] = undefined;
                 })
             );
         };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1ac0d28f5..f2c4cda34 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -126,6 +126,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
+        if (this.props.Document.isBackground) return false;
         const xf = this.getTransform();
         const xfo = this.getTransformOverlay();
         const [xp, yp] = xf.transformPoint(de.x, de.y);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2a11267d4..f484b6115 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -88,6 +88,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 width: this.width,
                 height: this.height,
                 zIndex: this.ZInd,
+                pointerEvents: this.props.Document.isBackground ? "none" : undefined
             }} >
 
             {!this.props.fitToBox ? <DocumentView {...this.props}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 43f4a0ba9..7bbf4a368 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -8,7 +8,7 @@
     transform-origin: top left;
 
     .imageBox-fader {
-        pointer-events: all;
+        pointer-events: inherit;
     }
 }
 
@@ -34,13 +34,12 @@
     height: 100%;
     max-width: 100%;
     max-height: 100%;
-    pointer-events: none;
+    pointer-events: inherit;
     background: transparent;
 
     img {
         height: auto;
         width: 100%;
-        pointer-events: all;
     }
 }
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 66b0e5a15..1951fcc1a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -416,7 +416,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             style={{
                 transform: `scale(${this.props.ContentScaling()})`,
                 width: `${100 / this.props.ContentScaling()}%`,
-                height: `${100 / this.props.ContentScaling()}%`
+                height: `${100 / this.props.ContentScaling()}%`,
+                pointerEvents: this.props.Document.isBackground ? "none" : undefined
             }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
-- 
cgit v1.2.3-70-g09d2


From 10476052270245c9f57979487336af40fb24139f Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 7 Feb 2020 15:17:06 -0500
Subject: fixed imageresize issue

---
 src/client/views/nodes/ImageBox.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 1951fcc1a..896ac1685 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -257,7 +257,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 }), 0);
             })
                 .catch((err: any) => console.log(err));
-        } else if (this.Document._nativeHeight !== cachedNativeSize.width || this.Document._nativeWidth !== cachedNativeSize.height) {
+        } else if (this.Document._nativeWidth !== cachedNativeSize.width || this.Document._nativeHeight !== cachedNativeSize.height) {
             !(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => {
                 if (!(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc)) {
                     this.Document._nativeWidth = cachedNativeSize.width;
-- 
cgit v1.2.3-70-g09d2


From 4af6916f1f68b879e11e38b4d2a3f9a0708ac979 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 11 Feb 2020 06:21:39 -0500
Subject: factored out image resizing, fixed back button, importer script,
 tooltips on range sliders

---
 src/client/documents/Documents.ts                  |  21 +-
 src/client/util/SettingsManager.scss               |   2 +-
 .../views/collections/CollectionTimeView.scss      |  17 +-
 .../views/collections/CollectionTimeView.tsx       |  12 +-
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/SliderBox-components.tsx    |  10 +-
 src/client/views/nodes/SliderBox.tsx               |  27 +-
 src/scraping/buxton/final/BuxtonImporter.ts        | 180 ++++----
 src/scraping/buxton/final/json/buxton.json         | 490 ++++++++++-----------
 src/server/DashUploadUtils.ts                      | 108 +++--
 10 files changed, 447 insertions(+), 422 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 027d7129e..49d1820f5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -355,13 +355,18 @@ export namespace Docs {
                 _LODdisable: true
             });
             Networking.FetchFromServer("/buxton").then(response => {
-                const parentProto = Doc.GetProto(parent);
-                parentProto.data = new List<Doc>();
                 const devices = JSON.parse(response);
                 if (!Array.isArray(devices)) {
-                    alert("Improper Buxton import formatting!");
+                    if ("error" in devices) {
+                        loading.title = devices.error;
+                    } else {
+                        console.log(devices);
+                        alert("The importer returned an unexpected import format. Check the console.");
+                    }
                     return;
                 }
+                const parentProto = Doc.GetProto(parent);
+                parentProto.data = new List<Doc>();
                 devices.forEach(device => {
                     const { __images } = device;
                     delete device.__images;
@@ -370,9 +375,9 @@ export namespace Docs {
                         const constructed = __images.map(relative => Utils.prepend(relative));
                         const deviceImages = constructed.map((url, i) => ImageDocument(url, { title: `image${i}.${extname(url)}` }));
                         const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true });
-                        const protoDoc = Doc.GetProto(doc);
-                        protoDoc.hero = new ImageField(constructed[0]);
-                        Docs.Get.DocumentHierarchyFromJson(device, undefined, protoDoc);
+                        const deviceProto = Doc.GetProto(doc);
+                        deviceProto.hero = new ImageField(constructed[0]);
+                        Docs.Get.DocumentHierarchyFromJson(device, undefined, deviceProto);
                         Doc.AddDocToList(parentProto, "data", doc);
                     }
                 });
@@ -507,10 +512,6 @@ export namespace Docs {
             return doc;
         }
 
-        export function IconDocument(icon: string, options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options);
-        }
-
         export function PdfDocument(url: string, options: DocumentOptions = {}) {
             return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options);
         }
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index 7a0fb0741..6513cb223 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -1,6 +1,6 @@
 @import "../views/globalCssVariables";
 
-.dialogue-box {
+.settings-interface {
     background-color: whitesmoke !important;
     color: grey;
     width: 450px;
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index 02ef4e2d2..2dffb3ea0 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -1,21 +1,26 @@
-.collectionTimeView, .collectionTimeView-pivot {
+.collectionTimeView,
+.collectionTimeView-pivot {
     display: flex;
     flex-direction: row;
     position: absolute;
     height: 100%;
     width: 100%;
     overflow: hidden;
+
     .collectionTimeView-backBtn {
         background: green;
         display: inline;
         margin-right: 20px;
     }
+
     .collectionFreeform-customText {
         text-align: left;
     }
+
     .collectionFreeform-customDiv {
         position: absolute;
     }
+
     .collectionTimeView-thumb {
         position: absolute;
         width: 30px;
@@ -28,14 +33,17 @@
         border-radius: 9px;
         opacity: 0.25;
     }
+
     .collectionTimeView-thumb-min {
-        margin-left:25%;
+        margin-left: 25%;
     }
+
     .collectionTimeView-thumb-max {
-        margin-left:75%;
+        margin-left: 75%;
     }
+
     .collectionTimeView-thumb-mid {
-        margin-left:50%;
+        margin-left: 50%;
     }
 
     .collectionTimeView-flyout {
@@ -118,6 +126,7 @@
         left: -10px;
     }
 }
+
 .collectionTimeView-pivot {
     .collectionFreeform-customText {
         text-align: center;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index db176d0bc..808144c18 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -46,7 +46,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
             this.props.Document._fitToBox = true;
         }
         if (!this.props.Document.onViewDefClick) {
-            this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" })
+            this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
         }
     }
 
@@ -164,7 +164,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
             typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
         Array.from(keySet).map(fieldKey =>
             docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" }));
-        docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" })
+        docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" });
         ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
         const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
         ContextMenu.Instance.displayMenu(x, y, ":");
@@ -282,9 +282,9 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
         const layoutItems: ContextMenuProps[] = [];
         const doc = this.props.Document;
 
-        layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline" }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot" }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined }, icon: "compress-arrows-alt" });
+        layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" });
+        layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" });
+        layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" });
         layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
 
         ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" });
@@ -331,7 +331,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
             <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
                 style={{ height: `calc(100%  - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
                 <div className={"pivotKeyEntry"}>
-                    <button className="collectionTimeView-backBtn" style={{ width: 50, height: 20, background: "green" }}
+                    <button className="collectionTimeView-backBtn"
                         onClick={action(() => {
                             let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex);
                             if (prevFilterIndex > 0) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 896ac1685..99c6fde34 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -207,7 +207,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             return url.href;
         } else if (url.href.indexOf(window.location.origin) === -1) {
             return Utils.CorsProxy(url.href);
-        } else if (!(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+        } else if (!/\.(png|jpg|jpeg|gif)$/.test(lower)) {
             return url.href;//Why is this here
         }
         const ext = path.extname(url.href);
diff --git a/src/client/views/nodes/SliderBox-components.tsx b/src/client/views/nodes/SliderBox-components.tsx
index a38cad459..874a1108f 100644
--- a/src/client/views/nodes/SliderBox-components.tsx
+++ b/src/client/views/nodes/SliderBox-components.tsx
@@ -47,12 +47,12 @@ export class TooltipRail extends Component<TooltipRailProps> {
 
     onMouseEnter = () => {
         document.addEventListener("mousemove", this.onMouseMove);
-    };
+    }
 
     onMouseLeave = () => {
         this.setState({ value: null, percent: null });
         document.removeEventListener("mousemove", this.onMouseMove);
-    };
+    }
 
     onMouseMove = (e: Event) => {
         const { activeHandleID, getEventData } = this.props;
@@ -62,7 +62,7 @@ export class TooltipRail extends Component<TooltipRailProps> {
         } else {
             this.setState(getEventData(e));
         }
-    };
+    }
 
     render() {
         const { value, percent } = this.state;
@@ -120,11 +120,11 @@ export class Handle extends Component<HandleProps> {
 
     onMouseEnter = () => {
         this.setState({ mouseOver: true });
-    };
+    }
 
     onMouseLeave = () => {
         this.setState({ mouseOver: false });
-    };
+    }
 
     render() {
         const {
diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx
index 2c9effe24..844d95d11 100644
--- a/src/client/views/nodes/SliderBox.tsx
+++ b/src/client/views/nodes/SliderBox.tsx
@@ -50,11 +50,11 @@ export class SliderBox extends DocComponent<FieldViewProps, SliderDocument>(Slid
     onChange = (values: readonly number[]) => runInAction(() => {
         this.Document._sliderMinThumb = values[0];
         this.Document._sliderMaxThumb = values[1];
-        Cast(this.Document.onThumbChanged, ScriptField, null)?.script.run({ range: values, this: this.props.Document })
+        Cast(this.Document.onThumbChanged, ScriptField, null)?.script.run({ range: values, this: this.props.Document });
     })
 
     render() {
-        const domain = [NumCast(this.props.Document._sliderMin), NumCast(this.props.Document._sliderMax)]
+        const domain = [NumCast(this.props.Document._sliderMin), NumCast(this.props.Document._sliderMax)];
         const defaultValues = [NumCast(this.props.Document._sliderMinThumb), NumCast(this.props.Document._sliderMaxThumb)];
         return (
             <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()}
@@ -76,15 +76,20 @@ export class SliderBox extends DocComponent<FieldViewProps, SliderDocument>(Slid
                         <Handles>
                             {({ handles, activeHandleID, getHandleProps }) => (
                                 <div className="slider-handles">
-                                    {handles.map(handle => (
-                                        <Handle
-                                            key={handle.id}
-                                            handle={handle}
-                                            domain={domain}
-                                            isActive={handle.id === activeHandleID}
-                                            getHandleProps={getHandleProps}
-                                        />
-                                    ))}
+                                    {handles.map((handle, i) => {
+                                        const value = i === 0 ? this.Document._sliderMinThumb : this.Document._sliderMaxThumb;
+                                        return (
+                                            <div title={String(value)}>
+                                                <Handle
+                                                    key={handle.id}
+                                                    handle={handle}
+                                                    domain={domain}
+                                                    isActive={handle.id === activeHandleID}
+                                                    getHandleProps={getHandleProps}
+                                                />
+                                            </div>
+                                        );
+                                    })}
                                 </div>
                             )}
                         </Handles>
diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts
index b8016be13..66d1e761a 100644
--- a/src/scraping/buxton/final/BuxtonImporter.ts
+++ b/src/scraping/buxton/final/BuxtonImporter.ts
@@ -11,58 +11,56 @@ const createImageSizeStream = require("image-size-stream");
 import { parseXml } from "libxmljs";
 import { strictEqual } from "assert";
 
+interface DocumentContents {
+    body: string;
+    imageUrls: string[];
+    hyperlinks: string[];
+    captions: string[];
+    embeddedFileNames: string[];
+}
+
 export interface DeviceDocument {
     title: string;
     shortDescription: string;
     longDescription: string;
     company: string;
     year: number;
-    originalPrice: number | "NFS";
+    originalPrice: number;
     degreesOfFreedom: number;
-    dimensions: string;
+    dimensions?: string;
     primaryKey: string;
     secondaryKey: string;
     attribute: string;
 }
 
-interface DocumentContents {
-    body: string;
-    imageUrls: string[];
-    hyperlinks: string[];
-    captions: Caption[];
-}
-
 interface AnalysisResult {
     device?: DeviceDocument;
     errors?: any;
 }
 
-type Converter<T> = (raw: string) => { transformed?: T, error?: string };
+type Transformer<T> = (raw: string) => { transformed?: T, error?: string };
 
 interface Processor<T> {
     exp: RegExp;
     matchIndex?: number;
-    transformer?: Converter<T>;
+    transformer?: Transformer<T>;
     required?: boolean;
 }
 
-interface Caption {
-    fileName: string;
-    caption: string;
-}
-
 namespace Utilities {
 
     export function numberValue(raw: string) {
         const transformed = Number(raw);
         if (isNaN(transformed)) {
-            return { error: `${transformed} cannot be parsed to a numeric value.` };
+            return { error: `${raw} cannot be parsed to a numeric value.` };
         }
         return { transformed };
     }
 
     export function collectUniqueTokens(raw: string) {
-        return { transformed: Array.from(new Set(raw.replace(/,|\s+and\s+/g, " ").split(/\s+/).map(token => token.toLowerCase().trim()))).map(capitalize).sort() };
+        const pieces = raw.replace(/,|\s+and\s+/g, " ").split(/\s+/).filter(piece => piece.length);
+        const unique = new Set(pieces.map(token => token.toLowerCase().trim()));
+        return { transformed: Array.from(unique).map(capitalize).sort() };
     }
 
     export function correctSentences(raw: string) {
@@ -79,6 +77,21 @@ namespace Utilities {
         return word.charAt(0).toUpperCase() + word.slice(1);
     }
 
+    export async function readAndParseXml(zip: any, relativePath: string) {
+        const contents = await new Promise<string>((resolve, reject) => {
+            let body = "";
+            zip.stream(relativePath, (error: any, stream: any) => {
+                if (error) {
+                    reject(error);
+                }
+                stream.on('data', (chunk: any) => body += chunk.toString());
+                stream.on('end', () => resolve(body));
+            });
+        });
+
+        return parseXml(contents);
+    }
+
 }
 
 const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([
@@ -145,6 +158,7 @@ const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([
     }],
 ]);
 
+const sourceDir = path.resolve(__dirname, "source");
 const outDir = path.resolve(__dirname, "json");
 const imageDir = path.resolve(__dirname, "../../../server/public/files/images/buxton");
 const successOut = "buxton.json";
@@ -152,19 +166,28 @@ const failOut = "incomplete.json";
 const deviceKeys = Array.from(RegexMap.keys());
 
 export default async function executeImport() {
-    [outDir, imageDir].forEach(dir => {
-        rimraf.sync(dir);
-        mkdirSync(dir);
-    });
-    return parseFiles();
+    try {
+        const contents = readdirSync(sourceDir);
+        const wordDocuments = contents.filter(file => /.*\.docx?$/.test(file)).map(file => `${sourceDir}/${file}`);
+        [outDir, imageDir].forEach(dir => {
+            rimraf.sync(dir);
+            mkdirSync(dir);
+        });
+        return parseFiles(wordDocuments);
+    } catch (e) {
+        const message = [
+            "Unable to find a source directory.",
+            "Please ensure that the following directory exists and is populated with Word documents:",
+            `${sourceDir}`
+        ].join('\n');
+        console.log(red(message));
+        return { error: message };
+    }
 }
 
-async function parseFiles(): Promise<DeviceDocument[]> {
-    const source = path.resolve(`${__dirname}/source`);
-    const candidates = readdirSync(source).filter(file => /.*\.docx?$/.test(file)).map(file => `${source}/${file}`);
-
-    const imported: any[] = [];
-    for (const filePath of candidates) {
+async function parseFiles(wordDocuments: string[]): Promise<DeviceDocument[]> {
+    const imported: { fileName: string, contents: DocumentContents }[] = [];
+    for (const filePath of wordDocuments) {
         const fileName = path.basename(filePath).replace("Bill_Notes_", "");
         console.log(cyan(`\nExtracting contents from ${fileName}...`));
         imported.push({ fileName, contents: await extractFileContents(filePath) });
@@ -183,7 +206,7 @@ async function parseFiles(): Promise<DeviceDocument[]> {
         }
     });
 
-    const total = candidates.length;
+    const total = wordDocuments.length;
     if (masterDevices.length + masterErrors.length !== total) {
         throw new Error(`Encountered a ${masterDevices.length} to ${masterErrors.length} mismatch in device / error split!`);
     }
@@ -196,47 +219,35 @@ async function parseFiles(): Promise<DeviceDocument[]> {
     return masterDevices;
 }
 
-async function readAndParseXml(zip: any, relativePath: string) {
-    const contents = await new Promise<string>((resolve, reject) => {
-        let body = "";
-        zip.stream(relativePath, (error: any, stream: any) => {
-            if (error) {
-                reject(error);
-            }
-            stream.on('data', (chunk: any) => body += chunk.toString());
-            stream.on('end', () => resolve(body));
-        });
-    });
-
-    return parseXml(contents);
-}
+const tableCellXPath = '//*[name()="w:tbl"]/*[name()="w:tr"]/*[name()="w:tc"]';
+const hyperlinkXPath = '//*[name()="Relationship" and contains(@Type, "hyperlink")]';
 
 async function extractFileContents(pathToDocument: string): Promise<DocumentContents> {
     console.log('Extracting text...');
-
     const zip = new StreamZip({ file: pathToDocument, storeEntries: true });
     await new Promise<void>(resolve => zip.on('ready', resolve));
 
     // extract the body of the document and, specifically, its captions
+    const document = await Utilities.readAndParseXml(zip, "word/document.xml");
+    const body = document.root()?.text() ?? "No body found. Check the import script's XML parser.";
+    const captions: string[] = [];
+    const embeddedFileNames: string[] = [];
+    const captionTargets = document.find(tableCellXPath).map(node => node.text());
 
-    const document = await readAndParseXml(zip, "word/document.xml");
-    const body = document.root()?.text() || "No body found.";
-    const captions: Caption[] = [];
-    const captionTargets = document.find('//*[name()="w:tbl"]/*[name()="w:tr"]/*[name()="w:tc"]').map(node => node.text());
     const { length } = captionTargets;
-
     strictEqual(length > 3, true, "No captions written.");
     strictEqual(length % 3 === 0, true, "Improper caption formatting.");
 
     for (let i = 3; i < captionTargets.length; i += 3) {
         const [image, fileName, caption] = captionTargets.slice(i, i + 3);
         strictEqual(image, "", `The image cell in one row was not the empty string: ${image}`);
-        captions.push({ fileName, caption });
+        captions.push(caption);
+        embeddedFileNames.push(fileName);
     }
 
     // extract all hyperlinks embedded in the document
-    const rels = await readAndParseXml(zip, "word/_rels/document.xml.rels");
-    const hyperlinks = rels.find('//*[name()="Relationship" and contains(@Type, "hyperlink")]').map(el => el.attrs()[2].value());
+    const rels = await Utilities.readAndParseXml(zip, "word/_rels/document.xml.rels");
+    const hyperlinks = rels.find(hyperlinkXPath).map(el => el.attrs()[2].value());
     console.log("Text extracted.");
 
     console.log("Beginning image extraction...");
@@ -245,16 +256,11 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont
 
     zip.close();
 
-    return { body, imageUrls, captions, hyperlinks };
+    return { body, imageUrls, captions, embeddedFileNames, hyperlinks };
 }
 
 const imageEntry = /^word\/media\/\w+\.(jpeg|jpg|png|gif)/;
-const { pngs, jpgs } = AcceptibleMedia;
-const pngOptions = {
-    compressionLevel: 9,
-    adaptiveFiltering: true,
-    force: true
-};
+
 interface Dimensions {
     width: number;
     height: number;
@@ -281,46 +287,23 @@ async function writeImages(zip: any): Promise<string[]> {
 
         const ext = `.${type}`;
         const generatedFileName = `upload_${Utils.GenerateGuid()}${ext}`;
-        for (const { resizer, suffix } of resizers(ext)) {
-            const resizedPath = path.resolve(imageDir, InjectSize(generatedFileName, suffix));
-            await new Promise<void>(async (resolve, reject) => {
-                const writeStream = createWriteStream(resizedPath);
-                const readStream = await streamImage();
-                let source = readStream;
-                if (resizer) {
-                    source = readStream.pipe(resizer.withMetadata());
-                }
-                const out = source.pipe(writeStream);
-                out.on("close", resolve);
-                out.on("error", reject);
-            });
-        }
+
+        await DashUploadUtils.outputResizedImages(streamImage, imageDir, generatedFileName, ext);
+
         imageUrls.push(`/files/images/buxton/${generatedFileName}`);
     }
 
     return imageUrls;
 }
 
-function resizers(ext: string): DashUploadUtils.ImageResizer[] {
-    return [
-        { suffix: SizeSuffix.Original },
-        ...Object.values(DashUploadUtils.Sizes).map(size => {
-            let initial = sharp().resize(size.width, undefined, { withoutEnlargement: true });
-            if (pngs.includes(ext)) {
-                initial = initial.png(pngOptions);
-            } else if (jpgs.includes(ext)) {
-                initial = initial.jpeg();
-            }
-            return {
-                resizer: initial,
-                suffix: size.suffix
-            };
-        })
-    ];
-}
-
-function analyze(fileName: string, { body, imageUrls, captions, hyperlinks }: DocumentContents): AnalysisResult {
-    const device: any = { hyperlinks };
+function analyze(fileName: string, contents: DocumentContents): AnalysisResult {
+    const { body, imageUrls, captions, hyperlinks, embeddedFileNames } = contents;
+    const device: any = {
+        hyperlinks,
+        captions,
+        embeddedFileNames,
+        __images: imageUrls
+    };
     const errors: any = { fileName };
 
     for (const key of deviceKeys) {
@@ -353,15 +336,6 @@ function analyze(fileName: string, { body, imageUrls, captions, hyperlinks }: Do
         return { errors };
     }
 
-    device.__images = imageUrls;
-
-    device.captions = [];
-    device.fileNames = [];
-    captions.forEach(({ caption, fileName }) => {
-        device.captions.push(caption);
-        device.fileNames.push(fileName);
-    });
-
     return { device };
 }
 
diff --git a/src/scraping/buxton/final/json/buxton.json b/src/scraping/buxton/final/json/buxton.json
index ba23bab66..5c2e2c90c 100644
--- a/src/scraping/buxton/final/json/buxton.json
+++ b/src/scraping/buxton/final/json/buxton.json
@@ -9,6 +9,19 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx",
             "3DCad_Brochure.pdf"
         ],
+        "captions": [
+            "The 3Dconnexion CadMan 3D Motion Controller, a 6DOF joystick.",
+            "Brochure for the CadMan 3D Motion Controller.(Click on image to access full document.)"
+        ],
+        "embeddedFileNames": [
+            "3DCad_0410.JPG",
+            "3DCad_Brochure.jpg"
+        ],
+        "__images": [
+            "/files/images/buxton/upload_19ce4ebd-f207-4724-b8bf-610c8e1cf322.png",
+            "/files/images/buxton/upload_4dde8de0-f1ec-4ffb-b7cc-57b8c4ae6c94.png",
+            "/files/images/buxton/upload_825e8ed8-063c-428b-917d-eb62ea218f05.png"
+        ],
         "title": "3Dconnexion CadMan 3D Motion Controller",
         "company": "3Dconnexion",
         "year": 2003,
@@ -24,77 +37,7 @@
             "dim_unit": "mm"
         },
         "shortDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan controller, which is also in the collection.",
-        "longDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller more affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan/SpaceMouse controller, which is also in the collection. Like the Magellan, this is an isometric rate-control joystick. That is, it rests in a neutral central position, not sending and signal. When a force is applied to it, it emits a signal indicating the direction and strength of that force. This signal can then be mapped to a parameter of a selected object, such as a sphere, and – for example – cause that sphere to rotate for as long as, and as fast as, and in the direction determined by, the duration, force, and direction of the applied force. When released, it springs back to neutral position. Note that the force does not need to be directed along a single DOF. In fact, a core feature of the device is that one can simultaneously and independently apply force that asserts control over more than one DOF, and furthermore, vary those forces dynamically. As an aid to understanding, let me walk through some of the underlying concepts at play here by using a more familiar device: a computer mouse. If you move a mouse in a forward/backward direction, the mouse pointer on the screen moves between the screen’s top and bottom. If you think of the screen as a piece of graph paper, that corresponds to moving along the “Y” axis. That is one degree of freedom. On the other hand, you could move the mouse left and right, which causes the mouse to move between the left and right side of the screen. That would correspond to moving along the graph paper’s “X” axis – a second degree of freedom. Yet, you can also move the mouse diagonally. This is an example of independently controlling two degrees of freedom. Now imagine that if you lifted your mouse off your desktop, that your computer could dynamically sense its height as you did so. This would constitute a “flying mouse” (the literal translation of the German word for a “Bat”, which Canadian colleague, Colin Ware, applied to just such a mouse which he built in 1988). If you moved your Bat vertically up and down, perpendicular to the desktop, you would be controlling movement along the “Z” axis - a third degree of freedom. Having already seen that we can move a mouse diagonally, we have established that we need not be constrained to only moving along a single axis. That extends to the movement of our Bat and movement along the “Z” axis. We can control our hand movement in dependently in any or all directions in 3D space. But how does one reconcile the fact that we call the CadMan a “3D controller, and yet also describe it as having 6 degrees of freedom? Yes, as described, our bat can fly in 3D, but on the other hand, its range of movement within those 3 dimensions is much richer. To demonstrate this, move your hand in 3D space on and above your desktop. However, do so keeping your palm flat, parallel to the desktop with your fingers pointing directly forward. In so doing, you are still moving in 3D. Now, while moving, twist your wrist, while moving the hand, such that your palm is alternatively exposed to the left and right side. This constitutes rotation around the “Y” axis. A fourth DOF. Now add a waving motion to your hand, as if it were a paper airplane diving up and down, while also rocking left and right. But keep your fingers pointing forward. You have now added a fifth DOF, rotation around the “X” axis. Finally, add a twist to your wrist so that your fingers are no longer constrained to pointing forward. This is the sixth degree of freedom, rotation around the “Z” axis. Now don’t be fooled, this exercise could continue. We are not restricted to even six DOF. Imagine doing the above, but where the movement and rotations are measured relative to the Bat’s position and orientation, rather than to the holding/controlling hand, per se. One could imagine the Bat having a scroll wheel, like the one on most mice today. Furthermore, while flying your Bat around in 3D, that wheel could easily be rolled in either forward or backward, and thereby control the size of whatever was being controlled. Hence, with one hand we could assert simultaneous and independent control over 7 DOF in 3D space. This exercise has two intended take-aways. The first is a better working understanding between the notion of Degree of Freedom (DOF) and Dimension in space. Hopefully, the confusion frequently encountered when 3D and 6DOF are used in close context, can now be eliminated. Second, is that, with appropriate sensing, the human hand is capable of exercising control over far more degrees of freedom that six. And if we use the two hands together, the potential number of DOF that one can control goes even further. Finally, it is important to add one more take-away – one which both emerges from, and is frequently encountered when discussing, the previous two. That is, do not equate exercising simultaneous control over a high number of DOF with consciously doing the same number of different things all at once. The example that used to be thrown at me when I started talking about coordinated simultaneously bi-manual action went along the lines of, “Psychology tells us that we cannot do multiple things at once, for example, simultaneously tapping your head and rubbing your stomach. ”Well, first, I can tap my head with one hand while rubbing my stomach with the other. But that is not the point. The whole essence of skill – motor-sensory and cognitive – is “chunking” or task integration. When one appears to be doing many different things at once, if they are skilled, they are consciously doing only one thing. Playing a chord on the piano, for example, or skiing down the hill. Likewise, in flying your imaginary BAT in the previous exercise with the scroll wheel, were you doing 7 things at once, or one thing with 7 DOF? And if you had a Bat in each hand, does that mean you are now doing 14 things at once, or are you doing one thing with 14 DOF? Let me provide a different way of answering this question: if you have ever played air guitar, or “conducted” the orchestra that you are listening to on the radio, you are exercising control over more than 14 DOF. And you are doing exactly what I just said, “playing air guitar” or “conducting an orchestra”. One thing – at the conscious level, which is what matters – despite almost any one thing being able to be deconstructed into hundreds of sub-tasks. As I said the essence of skill: aggregation, or chunking. What is most important for both tool designers and users to be mindful of, is the overwhelming influence that our choice and design of tools impacts the degree to which such integration or chunking can take place. The degree to which the tool matches both the skills that we have already acquired through a lifetime of living in the everyday world, and the demands of the intended task, the more seamless that task can be performed, the more “natural” it will feel, and the less learning will be required. In my experience, it brought particular value when used bimanually, in combination with a mouse, where the preferred hand performed conventional pointing, selection and dragging tasks, while the non-preferred hand could manipulate the parameters of the thing being selected. First variation of the since the 2001 formation of 3Dconnextion. The CadMan came in 5 colours: smoke, orange, red, blue and green. See the notes for the LogiCad3D Magellan for more details on this class of device. It is the “parent” of the CadMan, and despite the change in company name, it comes from the same team.",
-        "__images": [
-            "/files/images/buxton/upload_921b19f8-6cba-48c4-9a0b-8b40dbe225a7.png",
-            "/files/images/buxton/upload_1838dd5c-e37d-4715-8c60-6a5272a1aa4f.png",
-            "/files/images/buxton/upload_cbb2a272-572a-4e25-8802-b88d858a685d.png"
-        ],
-        "captions": [
-            "The 3Dconnexion CadMan 3D Motion Controller, a 6DOF joystick.",
-            "Brochure for the CadMan 3D Motion Controller.(Click on image to access full document.)"
-        ],
-        "fileNames": [
-            "3DCad_0410.JPG",
-            "3DCad_Brochure.jpg"
-        ]
-    },
-    {
-        "hyperlinks": [
-            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx",
-            "SpaceNavigator_Press_Release.pdf",
-            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx",
-            "https://web.archive.org/web/20061205222533/http://www.3dconnexion.com:80/products/3a1d.php",
-            "3DConnexion_SpaceNavigator/SpaceNavigator_Launch_Web_Page.pdf",
-            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx",
-            "https://www.pcmag.com/article2/0,2817,2082798,00.asp",
-            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx",
-            "SpaceNavigator_Launch_Web_Page.pdf",
-            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx",
-            "Navigator_Data_Sheet_2006.pdf",
-            "3DConnexion_SpaceNavigator/SpaceNavigator_Press_Release.pdf"
-        ],
-        "title": "3Dconnexion SpaceNavigator ",
-        "company": "3Dconnexion",
-        "year": 2006,
-        "primaryKey": "Joystick",
-        "secondaryKey": "Dial",
-        "attribute": "Isometric",
-        "originalPrice": 59,
-        "degreesOfFreedom": 6,
-        "dimensions": {
-            "dim_length": 78,
-            "dim_width": 78,
-            "dim_height": 53,
-            "dim_unit": "mm"
-        },
-        "shortDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market.",
-        "longDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market. As illustrated in an accompanying image, the direction of the force which controls each of the 6 degrees of freedom of the SpaceNavigator are: Move Left-Right: Push/Pull left-right parallel to the desktop. Move Forward-Backward: Push/Pull forward-backward parallel to the desktop. Move Vertically, Up-Down: Push down vertically into the table or pull up vertically away from the tableTilt Left-Right: Tilt the joystick left-rightTilt Forward-Backward: Tilt joystick forward-backwardRotate around vertical axis: Twist the joystick clockwise or counter clockwise. Control of these 6 DOF can be combined. For example, you can rotate/roll right while spinning. Besides gaming, one of the hopes was that this device would be used in interacting with 3D programs like Google Earth. The problem was, however, that there were few such programs then, just as now, relatively speaking, and even Google Earth, while remarkable, is not used anywhere as frequently of 2D Google Maps, for example. For those of us in the 3D graphics market, it was fantastic with respect to animation, games and industrial design. But it never took off, no matter how seductive it was. And, the interesting question is, will VR and AR change that? And if so, how will this class of 6DOF device play in that market?",
-        "__images": [
-            "/files/images/buxton/upload_814d9b07-b287-4bac-a77c-c792f9a27e7f.jpg",
-            "/files/images/buxton/upload_f1784ab5-5f31-4fd8-98a7-629531d160bc.png",
-            "/files/images/buxton/upload_dcc8f7f0-9878-46b9-b4e0-a37a0b625af8.png",
-            "/files/images/buxton/upload_86205b18-0e6e-4f33-b691-329dac5fcdd4.jpg",
-            "/files/images/buxton/upload_9d4aaae9-9ba0-4cf8-b3a8-b7fd9d2535cd.png",
-            "/files/images/buxton/upload_69510cf3-0e97-40b9-98b9-bd60eb6b52bb.png",
-            "/files/images/buxton/upload_cc289bd3-2ffa-49f3-ab03-ae869ede7a19.png"
-        ],
-        "captions": [
-            "The 3Dconnexion SpaceNavigator 6DOF Joystick.",
-            "The 3Dconnexion SpaceNavigator adjacent to ruler in order to show scale.",
-            "Diagram from SpaceNavigator Data Sheet illustrating the directions of force which control each of the 6DOF.",
-            "Page 1 of the SpaceNavigarot Data Sheet.(Click on image to access full document.)",
-            "A page from the launch web site of the SpaceNavigator.(Click on image to access full document.)",
-            "Front page of the press release announcing the launch of the SpaceNavigator.(Click on image to access full document.)"
-        ],
-        "fileNames": [
-            "SpaceNavigator_01.JPG",
-            "SpaceNavigator_02.JPG",
-            "SpaceNavigator_Control_Axes.jpg",
-            "Navigator_Data_Sheet_2006.jpg",
-            "SpaceNavigator_Launch_Web_Page.jpg",
-            "SpaceNavigator_Press_Release.jpg"
-        ]
+        "longDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller more affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan/SpaceMouse controller, which is also in the collection. Like the Magellan, this is an isometric rate-control joystick. That is, it rests in a neutral central position, not sending and signal. When a force is applied to it, it emits a signal indicating the direction and strength of that force. This signal can then be mapped to a parameter of a selected object, such as a sphere, and – for example – cause that sphere to rotate for as long as, and as fast as, and in the direction determined by, the duration, force, and direction of the applied force. When released, it springs back to neutral position. Note that the force does not need to be directed along a single DOF. In fact, a core feature of the device is that one can simultaneously and independently apply force that asserts control over more than one DOF, and furthermore, vary those forces dynamically. As an aid to understanding, let me walk through some of the underlying concepts at play here by using a more familiar device: a computer mouse. If you move a mouse in a forward/backward direction, the mouse pointer on the screen moves between the screen’s top and bottom. If you think of the screen as a piece of graph paper, that corresponds to moving along the “Y” axis. That is one degree of freedom. On the other hand, you could move the mouse left and right, which causes the mouse to move between the left and right side of the screen. That would correspond to moving along the graph paper’s “X” axis – a second degree of freedom. Yet, you can also move the mouse diagonally. This is an example of independently controlling two degrees of freedom. Now imagine that if you lifted your mouse off your desktop, that your computer could dynamically sense its height as you did so. This would constitute a “flying mouse” (the literal translation of the German word for a “Bat”, which Canadian colleague, Colin Ware, applied to just such a mouse which he built in 1988). If you moved your Bat vertically up and down, perpendicular to the desktop, you would be controlling movement along the “Z” axis - a third degree of freedom. Having already seen that we can move a mouse diagonally, we have established that we need not be constrained to only moving along a single axis. That extends to the movement of our Bat and movement along the “Z” axis. We can control our hand movement in dependently in any or all directions in 3D space. But how does one reconcile the fact that we call the CadMan a “3D controller, and yet also describe it as having 6 degrees of freedom? Yes, as described, our bat can fly in 3D, but on the other hand, its range of movement within those 3 dimensions is much richer. To demonstrate this, move your hand in 3D space on and above your desktop. However, do so keeping your palm flat, parallel to the desktop with your fingers pointing directly forward. In so doing, you are still moving in 3D. Now, while moving, twist your wrist, while moving the hand, such that your palm is alternatively exposed to the left and right side. This constitutes rotation around the “Y” axis. A fourth DOF. Now add a waving motion to your hand, as if it were a paper airplane diving up and down, while also rocking left and right. But keep your fingers pointing forward. You have now added a fifth DOF, rotation around the “X” axis. Finally, add a twist to your wrist so that your fingers are no longer constrained to pointing forward. This is the sixth degree of freedom, rotation around the “Z” axis. Now don’t be fooled, this exercise could continue. We are not restricted to even six DOF. Imagine doing the above, but where the movement and rotations are measured relative to the Bat’s position and orientation, rather than to the holding/controlling hand, per se. One could imagine the Bat having a scroll wheel, like the one on most mice today. Furthermore, while flying your Bat around in 3D, that wheel could easily be rolled in either forward or backward, and thereby control the size of whatever was being controlled. Hence, with one hand we could assert simultaneous and independent control over 7 DOF in 3D space. This exercise has two intended take-aways. The first is a better working understanding between the notion of Degree of Freedom (DOF) and Dimension in space. Hopefully, the confusion frequently encountered when 3D and 6DOF are used in close context, can now be eliminated. Second, is that, with appropriate sensing, the human hand is capable of exercising control over far more degrees of freedom that six. And if we use the two hands together, the potential number of DOF that one can control goes even further. Finally, it is important to add one more take-away – one which both emerges from, and is frequently encountered when discussing, the previous two. That is, do not equate exercising simultaneous control over a high number of DOF with consciously doing the same number of different things all at once. The example that used to be thrown at me when I started talking about coordinated simultaneously bi-manual action went along the lines of, “Psychology tells us that we cannot do multiple things at once, for example, simultaneously tapping your head and rubbing your stomach. ”Well, first, I can tap my head with one hand while rubbing my stomach with the other. But that is not the point. The whole essence of skill – motor-sensory and cognitive – is “chunking” or task integration. When one appears to be doing many different things at once, if they are skilled, they are consciously doing only one thing. Playing a chord on the piano, for example, or skiing down the hill. Likewise, in flying your imaginary BAT in the previous exercise with the scroll wheel, were you doing 7 things at once, or one thing with 7 DOF? And if you had a Bat in each hand, does that mean you are now doing 14 things at once, or are you doing one thing with 14 DOF? Let me provide a different way of answering this question: if you have ever played air guitar, or “conducted” the orchestra that you are listening to on the radio, you are exercising control over more than 14 DOF. And you are doing exactly what I just said, “playing air guitar” or “conducting an orchestra”. One thing – at the conscious level, which is what matters – despite almost any one thing being able to be deconstructed into hundreds of sub-tasks. As I said the essence of skill: aggregation, or chunking. What is most important for both tool designers and users to be mindful of, is the overwhelming influence that our choice and design of tools impacts the degree to which such integration or chunking can take place. The degree to which the tool matches both the skills that we have already acquired through a lifetime of living in the everyday world, and the demands of the intended task, the more seamless that task can be performed, the more “natural” it will feel, and the less learning will be required. In my experience, it brought particular value when used bimanually, in combination with a mouse, where the preferred hand performed conventional pointing, selection and dragging tasks, while the non-preferred hand could manipulate the parameters of the thing being selected. First variation of the since the 2001 formation of 3Dconnextion. The CadMan came in 5 colours: smoke, orange, red, blue and green. See the notes for the LogiCad3D Magellan for more details on this class of device. It is the “parent” of the CadMan, and despite the change in company name, it comes from the same team."
     },
     {
         "hyperlinks": [
@@ -107,6 +50,25 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx",
             "SpaceMouse_Plus_Info_Sheet.pdf"
         ],
+        "captions": [
+            "Overview of the Magellan Plus.",
+            "Overview of Magellan Plus in different colour.",
+            "Page one of the Magellan Plus / SpaceMouse Plus Data Sheet.(Click on image to access full document.)",
+            "Page one of the Magellan Plus / SpaceMouse Plus Information Sheet.(Click on image to access full document.)"
+        ],
+        "embeddedFileNames": [
+            "3DPlus_0396.JPG",
+            "3DPlus_0391.JPG",
+            "SpaceMouse_Plus_Data_Sheet.jpg",
+            "SpaceMouse_Plus_Info_Sheet.jpg"
+        ],
+        "__images": [
+            "/files/images/buxton/upload_f84cb54e-9bdb-4fbc-9503-6cfa8ddd65be.jpg",
+            "/files/images/buxton/upload_506b5361-a9fd-4440-a19b-5b4af461878a.png",
+            "/files/images/buxton/upload_60380383-3603-4624-98cf-50a0d4afe4c9.png",
+            "/files/images/buxton/upload_2fad6fb2-f1e1-4fe3-915b-33586f05fcad.png",
+            "/files/images/buxton/upload_effcd815-49eb-4cd4-82cf-2519e58eb729.png"
+        ],
         "title": "3Dconnexion Magellan/SpaceMouse Plus",
         "company": "3Dconnexion",
         "year": 1998,
@@ -122,26 +84,7 @@
             "dim_unit": "mm"
         },
         "shortDescription": "The Magellan/SpaceMouse Plus is a refinement of the original LogiCad3D Magellan. From the industrial design perspective, the main difference is the switch from the original round “hockey puck” shaped handle to this asymmetric one.",
-        "longDescription": "The Magellan Plus (also known as the Spacemouse Plus) is a refinement of the original LogiCad3D Magellan (LogiCad3D evolved into 3DConnexion). From the industrial design perspective, the main difference between the two is the switch from the round “hockey puck” shaped handle to this asymmetric one. Despite this rather small change, both are included in the collection since that small change is a good example of my axiom that “everything being best for something and worst for something else. ” One of the things which most attracted me to the original Magellan was its hockey-puck shaped handle. The reason is that in my mind, it shouted out, “jog-shuttle wheel”. That is, the kind of controller that I was familiar with from editing audio and video. Since we were building software for 3D animation, the shape had value as a physical icon, or “phycon”, whose affordances suggested how this new control could employ existing skills. That is the good side. On the other hand, for 3D manipulation, when gripped, that same symmetry lacked any immediate tactile feed-forward as to orientation. That is, what axis of the 3D model would be affected by tilting the handle in any direction. On the other hand, the new asymmetric handle told the user, through touch, how the orientation of the handle aligned with that of the 3D model being controlled. One of the key habits leading to design literacy is to constantly prospect for patterns, rather than just individual examples. The reg the pattern means that one can separate the superficial features of the example, and see the underlying issue. For example, compare the Magellan Plus and the LogiCad3D Magellan, respectfully, with any Apple mouse and the 1988 Apple iMac G3 “hockey puck” mouse. While mice and 3D joysticks are very different devices, the two pairings reflect the same pattern. The suggested lesson is that patterns suggest that certain things are not mere exceptions – they are something which will likely reoccur, and therefore something that one can learn from – as long as one can recognize the deep pattern hidden beneath the superficial exterior.",
-        "__images": [
-            "/files/images/buxton/upload_ea1384f6-9ab9-4a35-a252-94e066f646bc.jpg",
-            "/files/images/buxton/upload_748ba2c5-ed86-41d0-b531-7f2c18e552da.png",
-            "/files/images/buxton/upload_f45122ff-5057-4829-8659-3b8f1ecdb987.png",
-            "/files/images/buxton/upload_99cfa809-1205-4921-8616-f3a9677f4557.png",
-            "/files/images/buxton/upload_551cdd5c-1537-4a62-9cc0-b072d59546f9.png"
-        ],
-        "captions": [
-            "Overview of the Magellan Plus.",
-            "Overview of Magellan Plus in different colour.",
-            "Page one of the Magellan Plus / SpaceMouse Plus Data Sheet.(Click on image to access full document.)",
-            "Page one of the Magellan Plus / SpaceMouse Plus Information Sheet.(Click on image to access full document.)"
-        ],
-        "fileNames": [
-            "3DPlus_0396.JPG",
-            "3DPlus_0391.JPG",
-            "SpaceMouse_Plus_Data_Sheet.jpg",
-            "SpaceMouse_Plus_Info_Sheet.jpg"
-        ]
+        "longDescription": "The Magellan Plus (also known as the Spacemouse Plus) is a refinement of the original LogiCad3D Magellan (LogiCad3D evolved into 3DConnexion). From the industrial design perspective, the main difference between the two is the switch from the round “hockey puck” shaped handle to this asymmetric one. Despite this rather small change, both are included in the collection since that small change is a good example of my axiom that “everything being best for something and worst for something else. ” One of the things which most attracted me to the original Magellan was its hockey-puck shaped handle. The reason is that in my mind, it shouted out, “jog-shuttle wheel”. That is, the kind of controller that I was familiar with from editing audio and video. Since we were building software for 3D animation, the shape had value as a physical icon, or “phycon”, whose affordances suggested how this new control could employ existing skills. That is the good side. On the other hand, for 3D manipulation, when gripped, that same symmetry lacked any immediate tactile feed-forward as to orientation. That is, what axis of the 3D model would be affected by tilting the handle in any direction. On the other hand, the new asymmetric handle told the user, through touch, how the orientation of the handle aligned with that of the 3D model being controlled. One of the key habits leading to design literacy is to constantly prospect for patterns, rather than just individual examples. The reg the pattern means that one can separate the superficial features of the example, and see the underlying issue. For example, compare the Magellan Plus and the LogiCad3D Magellan, respectfully, with any Apple mouse and the 1988 Apple iMac G3 “hockey puck” mouse. While mice and 3D joysticks are very different devices, the two pairings reflect the same pattern. The suggested lesson is that patterns suggest that certain things are not mere exceptions – they are something which will likely reoccur, and therefore something that one can learn from – as long as one can recognize the deep pattern hidden beneath the superficial exterior."
     },
     {
         "hyperlinks": [
@@ -154,6 +97,31 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx",
             "SP_SB_SM_Feature_Comparison_001.pdf"
         ],
+        "captions": [
+            "Side view of the Spaceball 5000.",
+            "Upper-left view of the Spaceball 5000, showing the position of 9 of the 12 programable buttons.",
+            "Back view of the Spaceball 5000, showing the position of buttons on either side of the ball.",
+            "View showing bimanual use, with the SpaceBall in the left hand, mouse in right.  Note position of buttons relative to thumb and fingers.",
+            "Caption to come.(Click on image to access full document.)",
+            "Page 1 of SpaceBall 5000 Date Sheet.(Click on image to access full document.)"
+        ],
+        "embeddedFileNames": [
+            "3DSpace_5000_01.JPG",
+            "3DSpace_5000_02.JPG",
+            "3DSpace_5000_05.JPG",
+            "3DSpace_5000_06.JPG",
+            "SP_SB_SM_Feature_Comparison.jpg",
+            "SpaceBall_5000_Data_Sheet.jpg"
+        ],
+        "__images": [
+            "/files/images/buxton/upload_81972990-8cad-4299-926a-fe5d49711a80.jpg",
+            "/files/images/buxton/upload_04103620-72e4-431f-b34f-61d0f158277a.png",
+            "/files/images/buxton/upload_9bfe7c5a-1c5e-46e3-8f8d-4e0eec3e38e3.png",
+            "/files/images/buxton/upload_77da85f5-6575-4a8d-acb6-52f2df3307b0.png",
+            "/files/images/buxton/upload_1c041a33-b258-42d1-b9f2-d610a4838fb3.jpg",
+            "/files/images/buxton/upload_841f0d98-27d7-4298-a71d-c5cba72a262a.jpg",
+            "/files/images/buxton/upload_0e0eaf99-f8bb-4f02-ad46-651cd67c13b1.jpg"
+        ],
         "title": "3Dconnexion Spaceball 5000",
         "company": "3Dconnexion",
         "year": 2003,
@@ -169,32 +137,64 @@
             "dim_unit": "mm"
         },
         "shortDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00.",
-        "longDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00. This version of the SpaceBall illustrates how the form-factor of the original version has changed over time. There are now 12 programmable function keys, 9 to be operated by the fingers on one side, and 3 to be operated by the thumb on the other. Note how the button placement indicates that the device is intended to be used by the left hand, with an accompanying mouse by the right – that is, by being meant for the left hand, it is intended for a right handed person.",
-        "__images": [
-            "/files/images/buxton/upload_4be775c7-51a7-4f56-bb3b-a314330b7aca.jpg",
-            "/files/images/buxton/upload_86c5b76b-059e-42f1-bd6c-e46dd24d0247.png",
-            "/files/images/buxton/upload_39bad94f-6b05-4cf8-a801-ef12e30bc01e.png",
-            "/files/images/buxton/upload_b5a3f277-4123-4951-9cd5-5394301c783b.png",
-            "/files/images/buxton/upload_fdf810b2-dc9f-47a7-a9a1-730a0dd6c892.jpg",
-            "/files/images/buxton/upload_04cc5a6a-9481-4477-bb29-dd6959e9bf3e.jpg",
-            "/files/images/buxton/upload_061b9bec-c8da-4fdc-8587-125cfa686bc1.jpg"
+        "longDescription": "This is an improved version of the original 1991 SpaceBall, manufactured by SpaceBall Technologies. It is a good example of how products improve as the market grows, while the price goes down. The original model sold for $1, 595. 00 USD, while this for $499. 00. This version of the SpaceBall illustrates how the form-factor of the original version has changed over time. There are now 12 programmable function keys, 9 to be operated by the fingers on one side, and 3 to be operated by the thumb on the other. Note how the button placement indicates that the device is intended to be used by the left hand, with an accompanying mouse by the right – that is, by being meant for the left hand, it is intended for a right handed person."
+    },
+    {
+        "hyperlinks": [
+            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx",
+            "SpaceNavigator_Press_Release.pdf",
+            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx",
+            "https://web.archive.org/web/20061205222533/http://www.3dconnexion.com:80/products/3a1d.php",
+            "3DConnexion_SpaceNavigator/SpaceNavigator_Launch_Web_Page.pdf",
+            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx",
+            "https://www.pcmag.com/article2/0,2817,2082798,00.asp",
+            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx",
+            "SpaceNavigator_Launch_Web_Page.pdf",
+            "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx",
+            "Navigator_Data_Sheet_2006.pdf",
+            "3DConnexion_SpaceNavigator/SpaceNavigator_Press_Release.pdf"
         ],
         "captions": [
-            "Side view of the Spaceball 5000.",
-            "Upper-left view of the Spaceball 5000, showing the position of 9 of the 12 programable buttons.",
-            "Back view of the Spaceball 5000, showing the position of buttons on either side of the ball.",
-            "View showing bimanual use, with the SpaceBall in the left hand, mouse in right.  Note position of buttons relative to thumb and fingers.",
-            "Caption to come.(Click on image to access full document.)",
-            "Page 1 of SpaceBall 5000 Date Sheet.(Click on image to access full document.)"
+            "The 3Dconnexion SpaceNavigator 6DOF Joystick.",
+            "The 3Dconnexion SpaceNavigator adjacent to ruler in order to show scale.",
+            "Diagram from SpaceNavigator Data Sheet illustrating the directions of force which control each of the 6DOF.",
+            "Page 1 of the SpaceNavigarot Data Sheet.(Click on image to access full document.)",
+            "A page from the launch web site of the SpaceNavigator.(Click on image to access full document.)",
+            "Front page of the press release announcing the launch of the SpaceNavigator.(Click on image to access full document.)"
         ],
-        "fileNames": [
-            "3DSpace_5000_01.JPG",
-            "3DSpace_5000_02.JPG",
-            "3DSpace_5000_05.JPG",
-            "3DSpace_5000_06.JPG",
-            "SP_SB_SM_Feature_Comparison.jpg",
-            "SpaceBall_5000_Data_Sheet.jpg"
-        ]
+        "embeddedFileNames": [
+            "SpaceNavigator_01.JPG",
+            "SpaceNavigator_02.JPG",
+            "SpaceNavigator_Control_Axes.jpg",
+            "Navigator_Data_Sheet_2006.jpg",
+            "SpaceNavigator_Launch_Web_Page.jpg",
+            "SpaceNavigator_Press_Release.jpg"
+        ],
+        "__images": [
+            "/files/images/buxton/upload_b44ec511-334e-46c0-a315-63d90fac8117.jpg",
+            "/files/images/buxton/upload_734ae360-faaf-4c78-89b4-a3f120fe233e.png",
+            "/files/images/buxton/upload_d20bbf6a-7310-407a-8b85-c607e9cf7f73.png",
+            "/files/images/buxton/upload_e9236600-dd5f-4805-92ca-805d5a301aa1.jpg",
+            "/files/images/buxton/upload_c85c977d-ef99-44be-b55c-8e8dde124e83.png",
+            "/files/images/buxton/upload_a6034d66-7de6-4d4b-bd64-ea95cc00ae8a.png",
+            "/files/images/buxton/upload_07415a65-964a-4936-aa6e-79861e8ebcbf.png"
+        ],
+        "title": "3Dconnexion SpaceNavigator ",
+        "company": "3Dconnexion",
+        "year": 2006,
+        "primaryKey": "Joystick",
+        "secondaryKey": "Dial",
+        "attribute": "Isometric",
+        "originalPrice": 59,
+        "degreesOfFreedom": 6,
+        "dimensions": {
+            "dim_length": 78,
+            "dim_width": 78,
+            "dim_height": 53,
+            "dim_unit": "mm"
+        },
+        "shortDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market.",
+        "longDescription": "The SpaceNavigator is an entry level 6DOF joystick for the interactive 3D market. It came in a “Personal” and “Standard” edition, at $59. 00 and $99. 00 USD, respectively. These were break-through prices which opened up this technology (which cost $1, 595. 00 in 1991) to gamers and consumers. Doing so was necessary, since the high-end professional 3D graphics market was relatively small, and not growing anywhere near as fast as the consumer and gaming market. As illustrated in an accompanying image, the direction of the force which controls each of the 6 degrees of freedom of the SpaceNavigator are: Move Left-Right: Push/Pull left-right parallel to the desktop. Move Forward-Backward: Push/Pull forward-backward parallel to the desktop. Move Vertically, Up-Down: Push down vertically into the table or pull up vertically away from the tableTilt Left-Right: Tilt the joystick left-rightTilt Forward-Backward: Tilt joystick forward-backwardRotate around vertical axis: Twist the joystick clockwise or counter clockwise. Control of these 6 DOF can be combined. For example, you can rotate/roll right while spinning. Besides gaming, one of the hopes was that this device would be used in interacting with 3D programs like Google Earth. The problem was, however, that there were few such programs then, just as now, relatively speaking, and even Google Earth, while remarkable, is not used anywhere as frequently of 2D Google Maps, for example. For those of us in the 3D graphics market, it was fantastic with respect to animation, games and industrial design. But it never took off, no matter how seductive it was. And, the interesting question is, will VR and AR change that? And if so, how will this class of 6DOF device play in that market?"
     },
     {
         "hyperlinks": [
@@ -207,24 +207,6 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx",
             "https://web.archive.org/web/20070106100452/http://solutions.3m.com/wps/portal/3M/en_US/ergonomics/home/products/ergonomicmouse/factsheet/"
         ],
-        "title": "3M EM500 Ergonomic Mouse",
-        "company": "3M",
-        "year": 2006,
-        "primaryKey": "Mouse",
-        "secondaryKey": "Blank",
-        "attribute": "Blank",
-        "originalPrice": 72.5,
-        "degreesOfFreedom": 2,
-        "shortDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The compromise of this, however, is that one loses some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm.",
-        "longDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The joystick also came in two sizes so as to better accommodate different hand sizes. There almost always trade-offs in design. Often more than one. In this case, one compromise is a loss of some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm. Another is the increased time/attention required when moving from the keyboard to the mouse. To do a simple test, move your hand to a conventional mouse. Now place a water glass which is taller than it is wide in the same position as the mouse, and compare how fast you can get it “in hand” enough to control its movement as well as you could the mouse. Try the same thing while not looking – using motor memory. Then consider how often you make that change. And, by the same token, consider how many different ways you can hold your mouse while still using it, versus a joystick shaped mouse with the button on top. In none of this am I complaining about, nor criticizing this mouse design. Rather, I am trying to illustrate that there is a lot to consider, and as either a designer or consumer, these are things to train oneself to notice and question. From such experience emerges the basis for better choices in both design and purchase. .",
-        "__images": [
-            "/files/images/buxton/upload_5814f603-4190-41c8-992a-6c21feb72577.jpg",
-            "/files/images/buxton/upload_c35c732f-35de-42e3-a0e6-23eff29bc782.jpg",
-            "/files/images/buxton/upload_ea7e6086-5fdd-4974-bb43-b6a6e7cb9ee7.jpg",
-            "/files/images/buxton/upload_e02262d6-ca5c-428c-99eb-f87c2b4c02b0.jpg",
-            "/files/images/buxton/upload_f5a1c53d-813f-41ce-9823-16ebd363e4af.jpg",
-            "/files/images/buxton/upload_ec141ee2-594f-4e3f-b8b2-406a9d28438b.jpg"
-        ],
         "captions": [
             "A view of the 3M Ergonomic Mouse in the grasp of the right hand, with the thumb activating the button at the top of the stem.",
             "A view of the 3M Ergonomic Mouse standing alone.",
@@ -232,13 +214,31 @@
             "EM500 Ergonomic Mouse Fact Sheet from 3M web site, Jan, 2007",
             "Entry for the EM500 Ergonomic Mouse in the 2006 3M catalogue, p. 18.(To access full document, click on image.)"
         ],
-        "fileNames": [
+        "embeddedFileNames": [
             "3MErgo_01.JPG",
             "3MErgo_02.JPG",
             "3MErgo_Web_Jan_2007.JPG",
             "3MErgo_Web_Fact Sheet_Jan_2007.jpg",
             "3M_2006_Catalogue_p18.jpg"
-        ]
+        ],
+        "__images": [
+            "/files/images/buxton/upload_52b83c2a-5fe4-42aa-a488-5667b0a5beae.jpg",
+            "/files/images/buxton/upload_e05d8710-d82b-4f3b-8940-3745db1c855c.jpg",
+            "/files/images/buxton/upload_557c6fc7-04a3-4698-acde-4e53ceb6793c.jpg",
+            "/files/images/buxton/upload_d9723beb-bfe3-4556-b17d-aedf2db59777.jpg",
+            "/files/images/buxton/upload_5cb15815-3cab-406f-bf95-f9410997b4f4.jpg",
+            "/files/images/buxton/upload_a7fc91e0-7672-4f81-a8f3-66072997bd44.jpg"
+        ],
+        "title": "3M EM500 Ergonomic Mouse",
+        "company": "3M",
+        "year": 2006,
+        "primaryKey": "Mouse",
+        "secondaryKey": "Blank",
+        "attribute": "Blank",
+        "originalPrice": 72.5,
+        "degreesOfFreedom": 2,
+        "shortDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The compromise of this, however, is that one loses some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm.",
+        "longDescription": "Despite its form-factor suggesting that this is a joystick, it is actually a mouse. Ergonomic concerns drove this design. It forces the hand to assume a “thumb up” posture of the hand. This in turn reduces constriction of blood-flow through the relatively narrow channel of the wrist and reduces tension in the forearm. The joystick also came in two sizes so as to better accommodate different hand sizes. There almost always trade-offs in design. Often more than one. In this case, one compromise is a loss of some fine-motion control which might otherwise have been possible using the fingers, rather than relying more on the wrist and forearm. Another is the increased time/attention required when moving from the keyboard to the mouse. To do a simple test, move your hand to a conventional mouse. Now place a water glass which is taller than it is wide in the same position as the mouse, and compare how fast you can get it “in hand” enough to control its movement as well as you could the mouse. Try the same thing while not looking – using motor memory. Then consider how often you make that change. And, by the same token, consider how many different ways you can hold your mouse while still using it, versus a joystick shaped mouse with the button on top. In none of this am I complaining about, nor criticizing this mouse design. Rather, I am trying to illustrate that there is a lot to consider, and as either a designer or consumer, these are things to train oneself to notice and question. From such experience emerges the basis for better choices in both design and purchase. ."
     },
     {
         "hyperlinks": [
@@ -249,25 +249,6 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx",
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/browse.aspx"
         ],
-        "title": "Abaton ProPoint Optical Trackball",
-        "company": "Abaton",
-        "year": 1989,
-        "primaryKey": "Trackball",
-        "secondaryKey": "Blank",
-        "attribute": "Blank",
-        "originalPrice": 140,
-        "degreesOfFreedom": 2,
-        "shortDescription": "A relatively early trackball for the Apple Macintosh computer. Note the positioning of the buttons, which bias the device for right-handed use, and using the thumb for the buttons and fingers for manipulating the ball.",
-        "longDescription": "A relatively early trackball for the Apple Macintosh computer. The larger button was functionally equivalent to the mouse button, and the smaller one was a ‘lock’ button. The lock button is analogous to the SHIFT LOCK or CAPS LOCK on a QWERTY keyboard: With the keyboard, it means that one does not have to hold the SHIFT key down while typing a string of upper-case characters. With the trackball, it means that you don’t have to hold the primary “mouse” button down while rolling the trackball. The need that this meets can be easily be seen if one compares the relative difficulty of drawing a line by moving a mouse while holding down its button, compared to doing the same task with a trackball. With the mouse, the wrist and forearm mainly move the mouse, and the fingers are used to hold it, as well as the button. With the trackball, the fingers are engaged in rolling the ball as well as holding the button. Hence, the probability of task interference is high, just as with typing a string of upper-case characters on a keyboard without a SHIFT LOCK key. Next, in looking at trackballs, pay attention to the position of the buttons relative to the trackball itself. How this relationship varies across devices and says a lot about how the designers envisioned the device being used. For example, the relationship may indicate the design intends for the thumb or fingers to operate the ball. How does the relationship impact the device’s ability to accommodate left and right hand usage equally well? This latter point is especially important when the trackball is used simultaneously with a mouse. An example would be if the trackball was used to scroll a spreadsheet up-down / left-right, while the mouse was used to point, select, and/or drag. In this case, for example, the trackball would usually be operated by the non-dominant hand and the mouse by the dominant one. Yet, when used alone, the same user would typically operate the trackball with the dominant hand. The lesson from this example is the recognition that handedness is a factor of use (one vs two handed), not just a factor of whether the user’s left or right hand is dominant.",
-        "__images": [
-            "/files/images/buxton/upload_2e72df74-866e-44dc-b14a-a8bc5264b2ac.jpg",
-            "/files/images/buxton/upload_e0dd8776-b458-48d0-b381-6b4e371048d4.jpg",
-            "/files/images/buxton/upload_a87b19b5-4ed8-4f5f-a0bf-66c2396a3072.jpg",
-            "/files/images/buxton/upload_03993b48-4eb8-4236-9ccf-0d5f923f0be9.jpg",
-            "/files/images/buxton/upload_a734a81a-7c36-4e7d-b6f3-3360f7b3bc6e.jpg",
-            "/files/images/buxton/upload_7823b585-5306-4347-9f83-310ad9761515.jpg",
-            "/files/images/buxton/upload_4207fd37-2593-43c3-857e-9175056f28f3.jpg"
-        ],
         "captions": [
             "An upper-left view of the Abaton ProPoint trackball.",
             "Left side view of the Abaton ProPoint trackball showing the Apple Desktop Bus (ADB) socket.",
@@ -277,7 +258,7 @@
             "View of the Abaton ProPoint with the trackball removed.  The two shaft encoders that sense rotation in X and Y, respectively can be seen.",
             "First page of the Abatron ProPoint Trackball product sheet.(Click on image to access full document.)"
         ],
-        "fileNames": [
+        "embeddedFileNames": [
             "Abaton_0105.JPG",
             "Abaton_0098.JPG",
             "Abaton_0099.JPG",
@@ -285,7 +266,26 @@
             "Abaton_0103.JPG",
             "Abaton_0106.JPG",
             "Abaton_ProPoint_Brochure.jpg"
-        ]
+        ],
+        "__images": [
+            "/files/images/buxton/upload_f6f038a9-3583-4d21-9a43-f762e1df0b52.jpg",
+            "/files/images/buxton/upload_ad5f2bbd-66be-4dde-afe4-60e353a63f74.jpg",
+            "/files/images/buxton/upload_2e384260-5d2b-47a4-9508-028462a5a1e7.jpg",
+            "/files/images/buxton/upload_4dfcd3af-f06d-4c3e-8dff-7c43acd79a16.jpg",
+            "/files/images/buxton/upload_19eea9af-abec-4617-8b8d-fcb6637420de.jpg",
+            "/files/images/buxton/upload_89702b8a-fcb5-426d-b5af-c58e62d1c8ff.jpg",
+            "/files/images/buxton/upload_44419996-994e-471c-8dc7-cda22b00ac24.jpg"
+        ],
+        "title": "Abaton ProPoint Optical Trackball",
+        "company": "Abaton",
+        "year": 1989,
+        "primaryKey": "Trackball",
+        "secondaryKey": "Blank",
+        "attribute": "Blank",
+        "originalPrice": 140,
+        "degreesOfFreedom": 2,
+        "shortDescription": "A relatively early trackball for the Apple Macintosh computer. Note the positioning of the buttons, which bias the device for right-handed use, and using the thumb for the buttons and fingers for manipulating the ball.",
+        "longDescription": "A relatively early trackball for the Apple Macintosh computer. The larger button was functionally equivalent to the mouse button, and the smaller one was a ‘lock’ button. The lock button is analogous to the SHIFT LOCK or CAPS LOCK on a QWERTY keyboard: With the keyboard, it means that one does not have to hold the SHIFT key down while typing a string of upper-case characters. With the trackball, it means that you don’t have to hold the primary “mouse” button down while rolling the trackball. The need that this meets can be easily be seen if one compares the relative difficulty of drawing a line by moving a mouse while holding down its button, compared to doing the same task with a trackball. With the mouse, the wrist and forearm mainly move the mouse, and the fingers are used to hold it, as well as the button. With the trackball, the fingers are engaged in rolling the ball as well as holding the button. Hence, the probability of task interference is high, just as with typing a string of upper-case characters on a keyboard without a SHIFT LOCK key. Next, in looking at trackballs, pay attention to the position of the buttons relative to the trackball itself. How this relationship varies across devices and says a lot about how the designers envisioned the device being used. For example, the relationship may indicate the design intends for the thumb or fingers to operate the ball. How does the relationship impact the device’s ability to accommodate left and right hand usage equally well? This latter point is especially important when the trackball is used simultaneously with a mouse. An example would be if the trackball was used to scroll a spreadsheet up-down / left-right, while the mouse was used to point, select, and/or drag. In this case, for example, the trackball would usually be operated by the non-dominant hand and the mouse by the dominant one. Yet, when used alone, the same user would typically operate the trackball with the dominant hand. The lesson from this example is the recognition that handedness is a factor of use (one vs two handed), not just a factor of whether the user’s left or right hand is dominant."
     },
     {
         "hyperlinks": [
@@ -296,6 +296,18 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/contact.aspx",
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/acknowledgements.aspx"
         ],
+        "captions": [
+            "View of the Active Book prototype.",
+            "Front page of the Active Book brochure.(Click on image to access full document.)"
+        ],
+        "embeddedFileNames": [
+            "Active_1206.JPG",
+            "Active_Book_Brochure_p1.jpg"
+        ],
+        "__images": [
+            "/files/images/buxton/upload_2159da7b-9ebd-41ed-8b60-9eb36a6c6685.jpg",
+            "/files/images/buxton/upload_6d470914-fc41-4151-858b-f945807e62a2.png"
+        ],
         "title": "Active Book Company Active Book Prototype",
         "company": "Active Book Company",
         "year": 1991,
@@ -305,19 +317,7 @@
         "originalPrice": -1,
         "degreesOfFreedom": 2,
         "shortDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing.",
-        "longDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing. .",
-        "__images": [
-            "/files/images/buxton/upload_43360531-8b59-4c79-8c98-94f3949eb17a.jpg",
-            "/files/images/buxton/upload_1646f9f2-1afb-4d13-b773-65ef225b9670.png"
-        ],
-        "captions": [
-            "View of the Active Book prototype.",
-            "Front page of the Active Book brochure.(Click on image to access full document.)"
-        ],
-        "fileNames": [
-            "Active_1206.JPG",
-            "Active_Book_Brochure_p1.jpg"
-        ]
+        "longDescription": "This device is a prototype pen computer, The Active Book, which was developed in Cambridge, UK, by The Active Book Company. It had a strong focus on user experience, and like the equally ill-fated Momenta pen computer, was implemented using the pioneering object-oriented language Smalltalk. At about the time that this working prototype was built, near going into production, the company was bought and merged with EO, and the Active Book never went into production. This is an exceptionally rare piece of the history of pen computing. ."
     },
     {
         "hyperlinks": [
@@ -329,6 +329,25 @@
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx",
             "https://web.archive.org/web/20030507032656/http://www.adesso.us:80/product_details.asp?dept_id=106&pf_id=KA33ACK-540PB"
         ],
+        "captions": [
+            "Top view of the Adesso Mini-Touch Keyboard with Touchpad.",
+            "Side view of the Adesso Mini-Touch Keyboard with Touchpad.",
+            "Adesso 2003 product web page for the black 540_PB model of the Mini Touchpad Keyboard.",
+            "Adesso 2003 product web page for the white 540_PW model of the Mini Touchpad Keyboard"
+        ],
+        "embeddedFileNames": [
+            "Adesso_540_Top.JPG",
+            "Adesso_540_Side.JPG",
+            "Adesso_ACK-540PB_May_7_2003.jpg",
+            "Adesso_ACK-540PW_June 21_2003.jpg"
+        ],
+        "__images": [
+            "/files/images/buxton/upload_189469a3-1133-4a4f-8720-19c04f29684e.jpg",
+            "/files/images/buxton/upload_73f5d066-f9b5-41ce-9ce3-e27253ef51e7.jpg",
+            "/files/images/buxton/upload_498e5cbd-d65e-4b01-bed2-bcb2723ffee5.jpg",
+            "/files/images/buxton/upload_97ee9d03-07e0-448c-89f6-6ac61948de68.jpg",
+            "/files/images/buxton/upload_661e7387-dcd7-4ca1-8493-2e94f8803ef7.jpg"
+        ],
         "title": "Adesso ACK-540PB PS/2 Mini PS/2 Touchpad Keyboard",
         "company": "Adesso",
         "year": 2003,
@@ -344,26 +363,7 @@
             "dim_unit": "mm"
         },
         "shortDescription": "The Mini-Touch Keyboard is a small-footprint keyboard with a centrally mounted touchpad. It initially was released with a PS/2 connector, and then in 2006 the connector was updated to USB. While keyboards with integrated touchpads had been available since the mid-1980s, small-footprint ones with centrally mounted touch pads were far less common.",
-        "longDescription": "Released in 2003, this is a small add-on keyboard with an integrated touchpad. While there had been keyboards released with touchpads earlier – see the 1985 KeyTronic LT Touchpad Keyboard in the collection, for example – these were full-sized keyboard, typically with the touchpad mounted at the side, rather than the middle. Keyboards such as the Adesso ACK-540PB, were styled after the smaller foot-print keyboards then becoming standard on laptops, in terms of the central placement of the touchpad, as well as size. This central placement was significant, since it gave equal access to either right or left hand. The touchpad used was a Glidepoint, a 1994 stand-alone version of which is in the collection, the 1994 Cirque Glidepoint. This first model of the ACK-540 was released in both black (ACK-540PB) and white (ACK-540PW) and came with a PS/2 connector. In 2006, black and white versions updated with a USB connectors were released (the ACK-540UB and ACK-540UW) were released – an indication that the product had sustained a place in the market. These same keyboards were also marketed under different brand names, including SolidTek and Daltaco.",
-        "__images": [
-            "/files/images/buxton/upload_c0ded28c-3434-4782-ae83-a4d31846701a.jpg",
-            "/files/images/buxton/upload_1dc35a6f-c3d7-40e4-a633-f1ea2129c412.jpg",
-            "/files/images/buxton/upload_91f3276a-ec32-4e07-8001-35af3f16d1c3.jpg",
-            "/files/images/buxton/upload_76d9626e-1ff2-4062-8619-1e553db4cc5e.jpg",
-            "/files/images/buxton/upload_27702b70-daf7-4952-90ac-5b608d17c9eb.jpg"
-        ],
-        "captions": [
-            "Top view of the Adesso Mini-Touch Keyboard with Touchpad.",
-            "Side view of the Adesso Mini-Touch Keyboard with Touchpad.",
-            "Adesso 2003 product web page for the black 540_PB model of the Mini Touchpad Keyboard.",
-            "Adesso 2003 product web page for the white 540_PW model of the Mini Touchpad Keyboard"
-        ],
-        "fileNames": [
-            "Adesso_540_Top.JPG",
-            "Adesso_540_Side.JPG",
-            "Adesso_ACK-540PB_May_7_2003.jpg",
-            "Adesso_ACK-540PW_June 21_2003.jpg"
-        ]
+        "longDescription": "Released in 2003, this is a small add-on keyboard with an integrated touchpad. While there had been keyboards released with touchpads earlier – see the 1985 KeyTronic LT Touchpad Keyboard in the collection, for example – these were full-sized keyboard, typically with the touchpad mounted at the side, rather than the middle. Keyboards such as the Adesso ACK-540PB, were styled after the smaller foot-print keyboards then becoming standard on laptops, in terms of the central placement of the touchpad, as well as size. This central placement was significant, since it gave equal access to either right or left hand. The touchpad used was a Glidepoint, a 1994 stand-alone version of which is in the collection, the 1994 Cirque Glidepoint. This first model of the ACK-540 was released in both black (ACK-540PB) and white (ACK-540PW) and came with a PS/2 connector. In 2006, black and white versions updated with a USB connectors were released (the ACK-540UB and ACK-540UW) were released – an indication that the product had sustained a place in the market. These same keyboards were also marketed under different brand names, including SolidTek and Daltaco."
     },
     {
         "hyperlinks": [
@@ -378,35 +378,6 @@
             "https://web.archive.org/web/20071025131401/http://www.adesso.com/products_detail.asp?productid=363",
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx"
         ],
-        "title": "Adesso 2-in-1 Optical Keypad Calculator Mouse AKP-170",
-        "company": "Adesso Inc",
-        "year": 2007,
-        "primaryKey": "Mouse",
-        "secondaryKey": "Keypad",
-        "attribute": "",
-        "originalPrice": 29.99,
-        "degreesOfFreedom": 3,
-        "dimensions": {
-            "dim_length": 120.7,
-            "dim_width": 57.15,
-            "dim_height": 38.1,
-            "dim_unit": "mm"
-        },
-        "shortDescription": "This is a mouse / keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided.",
-        "longDescription": "This is a mouse / numerical keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided. There are a few mice with integrated keypads included in the collection. Each takes a different approach in terms of intent as well as industrial design. Comparing them is a worthwhile exercise. By the same token, the approach taken by this example echoes that taken by a very different product, but with the same intent: layer complexity. In this case the president from the collection is a TV/VCR remote control: the 1990 Sony RMT-V5A. For comparison, see the accompanying photo, and for more information, look at the Sony’s detailed device description. Finally, in drawing attention to this specific example rather than one of the hybrid mouse/keypads referred to earlier, the intent is to show that inspiration for design solutions for one class of device can come from those of very different categories. For sure, look at previous in-class solutions. But that just puts you on par with most other designers. The best exercise their creativity by looking in far less explored places. Note to the sharp eyed: If you look at the bottom of the mouse, you will see it marked model KM-1411, while I have been referring to it, as well as the Adesso Web Site, as model AKP-170. Rest assured, these are the same thing, as can be seen on the bottom right corner of the back of the box, where both numbers appear. I too am confused as to why, but also reassured. .",
-        "__images": [
-            "/files/images/buxton/upload_eb5b3f57-c610-457a-b18e-1b6f91bb3c66.jpg",
-            "/files/images/buxton/upload_1bdeb4b1-e49a-45af-b03f-60ab9012c0e2.jpg",
-            "/files/images/buxton/upload_2a3f9d15-de6c-45cc-ade3-6089fa11b0d1.jpg",
-            "/files/images/buxton/upload_f41374c2-ca7b-4502-b5b7-48100f7267a7.jpg",
-            "/files/images/buxton/upload_a7212e61-efa9-4010-ab02-da5a894d48ff.jpg",
-            "/files/images/buxton/upload_d8805592-045d-4911-a254-e7e7466f7e0a.jpg",
-            "/files/images/buxton/upload_9a75c468-9eab-4534-a64a-2bd62a890102.jpg",
-            "/files/images/buxton/upload_1af49b83-2e41-4b86-b4d4-3a7d6b6a8401.jpg",
-            "/files/images/buxton/upload_bdfe0b03-c8f4-4e07-9950-18cd6887a631.jpg",
-            "/files/images/buxton/upload_6b7f5a85-0d11-4188-a424-8c7521c17ade.jpg",
-            "/files/images/buxton/upload_773a12e4-59ae-410f-868b-e796ac050193.jpg"
-        ],
         "captions": [
             "An overview of the Adesso AKP-170 Mouse with the transparent hinged keypad cover open.  Note that the mouse controls can still be accessed.",
             "The Adesso AKP-170 Mouse with the transparent hinged keypad cover in its default closed position.",
@@ -420,7 +391,7 @@
             "Page 1 of a review of the Adesso AKP-170 from the Dec. 2007 Notebook Review web page.  (Click on image to access full document.)",
             "First page of Adesso Oct. 25, 2007 Product Web Page for the AKP-170 Mouse.(Click on image to access full document.)"
         ],
-        "fileNames": [
+        "embeddedFileNames": [
             "Adesso_KP_Mouse_01.JPG",
             "Adesso_KP_Mouse_02.JPG",
             "Adesso_KP_Mouse_03.JPG",
@@ -432,7 +403,36 @@
             "Adesso_KP_Mouse_09.JPG",
             "Adesso_KP_Mouse_10.JPG",
             "Adesso_KP_Mouse_11_Product.JPG"
-        ]
+        ],
+        "__images": [
+            "/files/images/buxton/upload_0a127e30-9014-4300-bade-07945fba940f.jpg",
+            "/files/images/buxton/upload_ca20611c-9061-4e56-9d72-4c4a3c798d3f.jpg",
+            "/files/images/buxton/upload_12b46021-eeef-47f0-9e55-ca9e9b76f9e4.jpg",
+            "/files/images/buxton/upload_5e86ad07-77af-4d3e-a82e-eaa9cafdbbe4.jpg",
+            "/files/images/buxton/upload_4be64833-53e9-4216-b7ec-d521e0b87933.jpg",
+            "/files/images/buxton/upload_7cb731b5-5c52-47e0-a33b-c113530478f2.jpg",
+            "/files/images/buxton/upload_b20407b3-3578-4fa5-8f16-e0bf729e4daa.jpg",
+            "/files/images/buxton/upload_bee2fcb8-702a-46aa-9ec3-eb9b50ef1ba7.jpg",
+            "/files/images/buxton/upload_0ee0f6d5-34fc-41f3-9c8e-cf462dbcc40b.jpg",
+            "/files/images/buxton/upload_8f442582-b901-4a38-8df6-5a9b3195df9f.jpg",
+            "/files/images/buxton/upload_3f96763c-c3af-46a7-8831-3206c8bc460e.jpg"
+        ],
+        "title": "Adesso 2-in-1 Optical Keypad Calculator Mouse AKP-170",
+        "company": "Adesso Inc",
+        "year": 2007,
+        "primaryKey": "Mouse",
+        "secondaryKey": "Keypad",
+        "attribute": "Blank",
+        "originalPrice": 29.99,
+        "degreesOfFreedom": 3,
+        "dimensions": {
+            "dim_length": 120.7,
+            "dim_width": 57.15,
+            "dim_height": 38.1,
+            "dim_unit": "mm"
+        },
+        "shortDescription": "This is a mouse / keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided.",
+        "longDescription": "This is a mouse / numerical keypad hybrid. With the transparent hinged cover down, it functions like a conventional scroll-wheel optical mouse. All the while, its additional capability as a numerical keypad / calculator is visible, and physically accessed by flipping up the cover. Since the design affords access to the mouse buttons and scroll wheel with the lid open, the opportunity to select cells in a spreadsheet, for example, and enter numbers without moving between the traditional keyboard and mouse is provided. There are a few mice with integrated keypads included in the collection. Each takes a different approach in terms of intent as well as industrial design. Comparing them is a worthwhile exercise. By the same token, the approach taken by this example echoes that taken by a very different product, but with the same intent: layer complexity. In this case the president from the collection is a TV/VCR remote control: the 1990 Sony RMT-V5A. For comparison, see the accompanying photo, and for more information, look at the Sony’s detailed device description. Finally, in drawing attention to this specific example rather than one of the hybrid mouse/keypads referred to earlier, the intent is to show that inspiration for design solutions for one class of device can come from those of very different categories. For sure, look at previous in-class solutions. But that just puts you on par with most other designers. The best exercise their creativity by looking in far less explored places. Note to the sharp eyed: If you look at the bottom of the mouse, you will see it marked model KM-1411, while I have been referring to it, as well as the Adesso Web Site, as model AKP-170. Rest assured, these are the same thing, as can be seen on the bottom right corner of the back of the box, where both numbers appear. I too am confused as to why, but also reassured. ."
     },
     {
         "hyperlinks": [
@@ -450,30 +450,6 @@
             "https://web.archive.org/web/20060211161019/http://www.a4tech.com/en/product2.asp?CID=90&SCID=92&MNO=NB-75",
             "http://research.microsoft.com/en-us/um/people/bibuxton/buxtoncollection/default.aspx"
         ],
-        "title": "A4 Tech BatteryFREE Wireless Optical Mouse Model NB-75D",
-        "company": "A4Tech",
-        "year": 2005,
-        "primaryKey": "Mouse",
-        "secondaryKey": "Scroll-wheel",
-        "attribute": "Blank",
-        "originalPrice": 35.99,
-        "degreesOfFreedom": 4,
-        "shortDescription": "This is a little-known innovative mouse worth study. It is one of the first to have wireless charging – by sitting on its mouse pad. It has two scroll wheels which are mounted at right angles – like an Etch-a-Sketch, to conform to the direction to be scrolled (up/down vs left/right. It also has a dedicated button which “double clicks” with one push.",
-        "longDescription": "This mouse fell below the radar, and yet it had remarkable innovations in its design, especially given its price. Like any mouse, it provided 2 degrees of freedom for pointing. Unlike most scroll-wheel mouse, it had two separate scroll wheels mounted at right angles. Hence, the orientation of each wheel provided a cue as to which direction the affected document would scroll – up/down or left/right. It is interesting to consider this scroll-wheel layout with that on a companion mouse, the A4 Tech Model IRW-5 4D Wireless Mouse, also in the collection, and shown side-by-side in one of the accompanying photographs. While the scroll wheels of the NF-75 are at right angles, those of the IRW-5 are parallel. Such differences should always provoke the question “Why? ” – for would-be designer and consumer alike. As a memory aid, and the conform to what psychologists call “stimulus-response (S: -R) compatibility”, the right-angle arrangement seems to be far better mapping of action to effect. On the other hand, try an experiment. First, place your hand, palm facing down, on a desktop as if you were holding a mouse. All fingers and thumb should be touching the desktop, but not your palm. Maintaining that position, left your index (scrolling) finger, and repeatedly “fold” and “unfold” it between a pointing posture, and touching your palm. Next, keeping your hand in the same position, this time, again extend your index finger. But this time, move its tip back-and-forth in a left-right motion. What I hope you quickly perceive is that the finger was “optimized” for the folding rather than lateral motion. You “feel” that from the difference in tension in the hand in the two cases of the “study”. The two conditions of our “quick” study roughly mimic the motor actions required to operate the scroll wheels. Since the brief test suggests the possibility that repeated lateral movement may accelerate the onset of repetitive stress injury, it would therefore also suggest that further studies should be undertaken before using a left-right scroll wheel – despite the acknowledged advantage with respect to S: R compatibility. In addition to the select button on virtually all mice, there was an additional button which issued a double-click on a single push. Hence, clicking one button would select, while pushing the other would select and open. While for many, the cost of an extra button may not seem worth saving the effort of a double click, those with some motor impairment may disagree. This is an example where knowing about such things may trigger useful insights to improve future designs. However, perhaps the biggest innovation with this line of A4 mice is that they were powered wirelessly. That is, there were no batteries in the mouse that might die at the wrong moment. Power was provided wirelessly from the mouse pad. Yet, one again we see that design is full of trade-offs. The cost of providing this feature is that of requiring not just the mouse pad, but a special one – something not overly attractive to road-warriors whose brief-cases were already weighted down by cables and other paraphernalia which took up more space than the laptop itself. .",
-        "__images": [
-            "/files/images/buxton/upload_7ee8bd93-1c84-4c0d-ad26-6de00b3fb54b.jpg",
-            "/files/images/buxton/upload_21bc883e-579b-4a4d-bad6-12ad864d6b2f.jpg",
-            "/files/images/buxton/upload_647d0ee6-496f-407b-a6a7-258715451ce2.jpg",
-            "/files/images/buxton/upload_2cf9a442-2385-42fd-9025-6d22e0f4892b.jpg",
-            "/files/images/buxton/upload_cf894eb9-79e4-4930-8805-34a32a62a18d.jpg",
-            "/files/images/buxton/upload_c54f6c79-f94d-4449-bc31-eff766d904f2.jpg",
-            "/files/images/buxton/upload_ca8a5100-2380-4618-b07c-083052cb3de9.jpg",
-            "/files/images/buxton/upload_510e561f-48f8-4b80-89dc-e54bfd2324c7.jpg",
-            "/files/images/buxton/upload_2854d46b-6672-471f-9608-23485c2949c0.jpg",
-            "/files/images/buxton/upload_988f4c9a-9b46-46b5-8152-ee8011ba2b9e.jpg",
-            "/files/images/buxton/upload_fc74e9b8-cb23-4be7-95f2-9efd0c1be967.jpg",
-            "/files/images/buxton/upload_6c2339a5-0f99-47ad-a082-bf2b4ca8f811.jpg"
-        ],
         "captions": [
             "The A4 Tech Model NB-75D optical mouse on its charging mouse pad.",
             "Front view of the A4 Tech NB-75D mouse beside its A4 Tech IRW-5 4D companion. Note the difference in the second scroll-wheel.  See text for discussion.",
@@ -487,7 +463,7 @@
             "Front page of Jandata sheet for the A4 BATTERYfree optical mouse product line. From A4 Tech web site.",
             "The A4 Tech Model NB-75D User’s Guide.(Click on image to access full document.)"
         ],
-        "fileNames": [
+        "embeddedFileNames": [
             "NB75D_IMG_0682.JPG",
             "NB75D_IMG_0666.JPG",
             "NB75D_IMG_0676.JPG",
@@ -499,6 +475,30 @@
             "NB75D_Product_Page.jpg",
             "BATTERYfree_Mouse_Data_Sheet.jpg",
             "NB75D_Mouse_Manual.jpg"
-        ]
+        ],
+        "__images": [
+            "/files/images/buxton/upload_dae8f950-38bf-4fdf-8db0-153e3cae9cae.jpg",
+            "/files/images/buxton/upload_3e054d83-d01f-4d3d-aad2-fa8ee1d22a97.jpg",
+            "/files/images/buxton/upload_e9c2ddb0-0684-4bb4-a88c-5ee8074e456e.jpg",
+            "/files/images/buxton/upload_9c829a69-7d45-4d25-b906-98153aa03001.jpg",
+            "/files/images/buxton/upload_4faa425c-a100-4ce4-869c-a6c6705779ee.jpg",
+            "/files/images/buxton/upload_ed20ac31-69f6-45b8-b8e0-b8ad829ba002.jpg",
+            "/files/images/buxton/upload_f3d3d8f5-01cf-4cd1-9d1c-0a11a8c3e239.jpg",
+            "/files/images/buxton/upload_af1fa3b6-4537-416a-b626-34bed1154f35.jpg",
+            "/files/images/buxton/upload_5127f377-b697-4ad7-9fb3-fd6f82017c3c.jpg",
+            "/files/images/buxton/upload_061ae6a2-e083-492a-bc8a-826c4be00790.jpg",
+            "/files/images/buxton/upload_32e91966-7f1b-4cd7-858b-9cdb63affd13.jpg",
+            "/files/images/buxton/upload_ccc9de21-b047-4f9c-89c6-08fb3d206c7a.jpg"
+        ],
+        "title": "A4 Tech BatteryFREE Wireless Optical Mouse Model NB-75D",
+        "company": "A4Tech",
+        "year": 2005,
+        "primaryKey": "Mouse",
+        "secondaryKey": "Scroll-wheel",
+        "attribute": "Blank",
+        "originalPrice": 35.99,
+        "degreesOfFreedom": 4,
+        "shortDescription": "This is a little-known innovative mouse worth study. It is one of the first to have wireless charging – by sitting on its mouse pad. It has two scroll wheels which are mounted at right angles – like an Etch-a-Sketch, to conform to the direction to be scrolled (up/down vs left/right. It also has a dedicated button which “double clicks” with one push.",
+        "longDescription": "This mouse fell below the radar, and yet it had remarkable innovations in its design, especially given its price. Like any mouse, it provided 2 degrees of freedom for pointing. Unlike most scroll-wheel mouse, it had two separate scroll wheels mounted at right angles. Hence, the orientation of each wheel provided a cue as to which direction the affected document would scroll – up/down or left/right. It is interesting to consider this scroll-wheel layout with that on a companion mouse, the A4 Tech Model IRW-5 4D Wireless Mouse, also in the collection, and shown side-by-side in one of the accompanying photographs. While the scroll wheels of the NF-75 are at right angles, those of the IRW-5 are parallel. Such differences should always provoke the question “Why? ” – for would-be designer and consumer alike. As a memory aid, and the conform to what psychologists call “stimulus-response (S: -R) compatibility”, the right-angle arrangement seems to be far better mapping of action to effect. On the other hand, try an experiment. First, place your hand, palm facing down, on a desktop as if you were holding a mouse. All fingers and thumb should be touching the desktop, but not your palm. Maintaining that position, left your index (scrolling) finger, and repeatedly “fold” and “unfold” it between a pointing posture, and touching your palm. Next, keeping your hand in the same position, this time, again extend your index finger. But this time, move its tip back-and-forth in a left-right motion. What I hope you quickly perceive is that the finger was “optimized” for the folding rather than lateral motion. You “feel” that from the difference in tension in the hand in the two cases of the “study”. The two conditions of our “quick” study roughly mimic the motor actions required to operate the scroll wheels. Since the brief test suggests the possibility that repeated lateral movement may accelerate the onset of repetitive stress injury, it would therefore also suggest that further studies should be undertaken before using a left-right scroll wheel – despite the acknowledged advantage with respect to S: R compatibility. In addition to the select button on virtually all mice, there was an additional button which issued a double-click on a single push. Hence, clicking one button would select, while pushing the other would select and open. While for many, the cost of an extra button may not seem worth saving the effort of a double click, those with some motor impairment may disagree. This is an example where knowing about such things may trigger useful insights to improve future designs. However, perhaps the biggest innovation with this line of A4 mice is that they were powered wirelessly. That is, there were no batteries in the mouse that might die at the wrong moment. Power was provided wirelessly from the mouse pad. Yet, one again we see that design is full of trade-offs. The cost of providing this feature is that of requiring not just the mouse pad, but a special one – something not overly attractive to road-warriors whose brief-cases were already weighted down by cables and other paraphernalia which took up more space than the laptop itself. ."
     }
 ]
\ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 26d6423e9..4e6bbaa3f 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -14,6 +14,7 @@ import { ParsedPDF } from "../server/PdfTypes";
 const parse = require('pdf-parse');
 import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager';
 import { red } from 'colors';
+import { Writable } from 'stream';
 const requestImageSize = require("../client/util/request-image-size");
 
 export enum SizeSuffix {
@@ -79,7 +80,7 @@ export namespace DashUploadUtils {
         switch (category) {
             case "image":
                 if (imageFormats.includes(format)) {
-                    const results = await UploadImage(path, basename(path), format);
+                    const results = await UploadImage(path, basename(path));
                     return { ...results, name, type };
                 }
             case "video":
@@ -108,8 +109,6 @@ export namespace DashUploadUtils {
         return MoveParsedFile(absolutePath, Directory.pdfs);
     }
 
-    const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
-
     /**
      * Uploads an image specified by the @param source to Dash's /public/files/
      * directory, and returns information generated during that upload 
@@ -127,12 +126,12 @@ export namespace DashUploadUtils {
      * 3) the size of the image, in bytes (4432130)
      * 4) the content type of the image, i.e. image/(jpeg | png | ...)
      */
-    export const UploadImage = async (source: string, filename?: string, format?: string, prefix: string = ""): Promise<ImageUploadInformation | Error> => {
+    export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<ImageUploadInformation | Error> => {
         const metadata = await InspectImage(source);
         if (metadata instanceof Error) {
             return metadata;
         }
-        return UploadInspectedImage(metadata, filename || metadata.filename, format, prefix);
+        return UploadInspectedImage(metadata, filename || metadata.filename, prefix);
     };
 
     export interface InspectionResults {
@@ -224,43 +223,24 @@ export namespace DashUploadUtils {
         });
     }
 
-    export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
+    export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<ImageUploadInformation> => {
         const { requestable, source, ...remaining } = metadata;
-        const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
-        const resolved = filename || generate(prefix, extension);
+        const extension = `.${remaining.contentType.split("/")[1].toLowerCase()}`;
+        const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${extension}`;
         const information: ImageUploadInformation = {
             clientAccessPath: clientPathToFile(Directory.images, resolved),
             serverAccessPaths: {},
             ...remaining
         };
-        const { pngs, jpgs } = AcceptibleMedia;
-        return new Promise<ImageUploadInformation>(async (resolve, reject) => {
-            const resizers = [
-                { resizer: sharp().rotate(), suffix: SizeSuffix.Original },
-                ...Object.values(Sizes).map(size => ({
-                    resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
-                    suffix: size.suffix
-                }))
-            ];
-            if (pngs.includes(extension)) {
-                resizers.forEach(element => element.resizer = element.resizer.png());
-            } else if (jpgs.includes(extension)) {
-                resizers.forEach(element => element.resizer = element.resizer.jpeg());
-            }
-            for (const { resizer, suffix } of resizers) {
-                await new Promise<void>(resolve => {
-                    const filename = InjectSize(resolved, suffix);
-                    information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
-                    request(requestable).pipe(resizer).pipe(createWriteStream(serverPathToFile(Directory.images, filename)))
-                        .on('close', resolve)
-                        .on('error', reject);
-                });
-            }
-            if (isLocal().test(source)) {
-                unlinkSync(source);
-            }
-            resolve(information);
-        });
+        const outputPath = pathToDirectory(Directory.images);
+        const writtenFiles = await outputResizedImages(() => request(requestable), outputPath, resolved, extension);
+        for (const suffix of Object.keys(writtenFiles)) {
+            information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, writtenFiles[suffix]);
+        }
+        if (isLocal().test(source)) {
+            unlinkSync(source);
+        }
+        return information;
     };
 
     const parseExifData = async (source: string): Promise<EnrichedExifData> => {
@@ -276,4 +256,60 @@ export namespace DashUploadUtils {
         });
     };
 
+    const { pngs, jpgs } = AcceptibleMedia;
+    const pngOptions = {
+        compressionLevel: 9,
+        adaptiveFiltering: true,
+        force: true
+    };
+
+    export interface ReadStreamLike {
+        pipe: (dest: Writable) => Writable;
+    }
+
+    export async function outputResizedImages(readStreamSource: () => ReadStreamLike | Promise<ReadStreamLike>, outputPath: string, fileName: string, ext: string) {
+        const writtenFiles: { [suffix: string]: string } = {};
+        for (const { resizer, suffix } of resizers(ext)) {
+            const resizedPath = path.resolve(outputPath, InjectSize(fileName, suffix));
+            writtenFiles[suffix] = resizedPath;
+            await new Promise<void>(async (resolve, reject) => {
+                const writeStream = createWriteStream(resizedPath);
+                let readStream: ReadStreamLike;
+                const source = readStreamSource();
+                if (source instanceof Promise) {
+                    readStream = await source;
+                } else {
+                    readStream = source;
+                }
+                if (resizer) {
+                    readStream = readStream.pipe(resizer.withMetadata());
+                }
+                const out = readStream.pipe(writeStream);
+                out.on("close", resolve);
+                out.on("error", reject);
+            });
+        }
+        return writtenFiles;
+    }
+
+    function resizers(ext: string): DashUploadUtils.ImageResizer[] {
+        return [
+            { suffix: SizeSuffix.Original },
+            ...Object.values(DashUploadUtils.Sizes).map(size => {
+                let initial: sharp.Sharp | undefined = sharp().resize(size.width, undefined, { withoutEnlargement: true });
+                if (pngs.includes(ext)) {
+                    initial = initial.png(pngOptions);
+                } else if (jpgs.includes(ext)) {
+                    initial = initial.jpeg();
+                } else {
+                    initial = undefined;
+                }
+                return {
+                    resizer: initial,
+                    suffix: size.suffix
+                };
+            })
+        ];
+    }
+
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 07fa40d04c7cc7b0006da69648c13ee54bcd4d27 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 11 Feb 2020 10:45:05 -0500
Subject: fixes for chrome for presBox

---
 package-lock.json                                  | 95 +++++++++++++---------
 src/client/views/MainView.tsx                      |  1 -
 src/client/views/TemplateMenu.tsx                  |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  1 -
 src/client/views/collections/CollectionView.tsx    | 12 +--
 .../CollectionMultirowView.tsx                     |  2 +
 src/client/views/nodes/FormattedTextBox.tsx        |  3 +-
 src/client/views/nodes/ImageBox.tsx                |  3 +-
 src/client/views/nodes/PresBox.tsx                 |  1 +
 src/client/views/nodes/VideoBox.tsx                |  3 +-
 src/client/views/nodes/WebBox.tsx                  |  3 +-
 src/client/views/pdf/PDFViewer.tsx                 |  3 +-
 src/new_fields/documentSchemas.ts                  | 20 +++--
 .../authentication/models/current_user_utils.ts    |  4 +-
 14 files changed, 85 insertions(+), 68 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/package-lock.json b/package-lock.json
index a09df33da..2636d7113 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -657,7 +657,7 @@
         },
         "@types/passport": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.0.tgz",
+            "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.2.tgz",
             "integrity": "sha512-Pf39AYKf8q+YoONym3150cEwfUD66dtwHJWvbeOzKxnA0GZZ/vAXhNWv9vMhKyRQBQZiQyWQnhYBEBlKW6G8wg==",
             "requires": {
                 "@types/express": "*"
@@ -1997,7 +1997,7 @@
                 },
                 "util": {
                     "version": "0.10.3",
-                    "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+                    "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
                     "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
                     "dev": true,
                     "requires": {
@@ -2585,7 +2585,7 @@
         },
         "browserify-aes": {
             "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+            "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
             "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
             "requires": {
                 "buffer-xor": "^1.0.3",
@@ -2619,7 +2619,7 @@
         },
         "browserify-rsa": {
             "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
             "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -2783,7 +2783,7 @@
         },
         "camelcase-keys": {
             "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
             "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
             "requires": {
                 "camelcase": "^2.0.0",
@@ -3576,7 +3576,7 @@
         },
         "create-hash": {
             "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+            "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
             "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
             "requires": {
                 "cipher-base": "^1.0.1",
@@ -3588,7 +3588,7 @@
         },
         "create-hmac": {
             "version": "1.1.7",
-            "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+            "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
             "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
             "requires": {
                 "cipher-base": "^1.0.3",
@@ -4126,7 +4126,7 @@
         },
         "diffie-hellman": {
             "version": "5.0.3",
-            "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+            "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
             "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
             "requires": {
                 "bn.js": "^4.1.0",
@@ -5406,7 +5406,8 @@
                 },
                 "ansi-regex": {
                     "version": "2.1.1",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "aproba": {
                     "version": "1.2.0",
@@ -5424,11 +5425,13 @@
                 },
                 "balanced-match": {
                     "version": "1.0.0",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "brace-expansion": {
                     "version": "1.1.11",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "balanced-match": "^1.0.0",
                         "concat-map": "0.0.1"
@@ -5441,15 +5444,18 @@
                 },
                 "code-point-at": {
                     "version": "1.1.0",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "concat-map": {
                     "version": "0.0.1",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "console-control-strings": {
                     "version": "1.1.0",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "core-util-is": {
                     "version": "1.0.2",
@@ -5552,7 +5558,8 @@
                 },
                 "inherits": {
                     "version": "2.0.4",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "ini": {
                     "version": "1.3.5",
@@ -5562,6 +5569,7 @@
                 "is-fullwidth-code-point": {
                     "version": "1.0.0",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "number-is-nan": "^1.0.0"
                     }
@@ -5574,17 +5582,20 @@
                 "minimatch": {
                     "version": "3.0.4",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "brace-expansion": "^1.1.7"
                     }
                 },
                 "minimist": {
                     "version": "0.0.8",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "minipass": {
                     "version": "2.9.0",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "safe-buffer": "^5.1.2",
                         "yallist": "^3.0.0"
@@ -5601,6 +5612,7 @@
                 "mkdirp": {
                     "version": "0.5.1",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "minimist": "0.0.8"
                     }
@@ -5681,7 +5693,8 @@
                 },
                 "number-is-nan": {
                     "version": "1.0.1",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "object-assign": {
                     "version": "4.1.1",
@@ -5691,6 +5704,7 @@
                 "once": {
                     "version": "1.4.0",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "wrappy": "1"
                     }
@@ -5766,7 +5780,8 @@
                 },
                 "safe-buffer": {
                     "version": "5.1.2",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "safer-buffer": {
                     "version": "2.1.2",
@@ -5796,6 +5811,7 @@
                 "string-width": {
                     "version": "1.0.2",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "code-point-at": "^1.0.0",
                         "is-fullwidth-code-point": "^1.0.0",
@@ -5813,6 +5829,7 @@
                 "strip-ansi": {
                     "version": "3.0.1",
                     "bundled": true,
+                    "optional": true,
                     "requires": {
                         "ansi-regex": "^2.0.0"
                     }
@@ -5851,11 +5868,13 @@
                 },
                 "wrappy": {
                     "version": "1.0.2",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 },
                 "yallist": {
                     "version": "3.1.1",
-                    "bundled": true
+                    "bundled": true,
+                    "optional": true
                 }
             }
         },
@@ -6975,7 +6994,7 @@
         },
         "is-accessor-descriptor": {
             "version": "0.1.6",
-            "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+            "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
             "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7030,7 +7049,7 @@
         },
         "is-data-descriptor": {
             "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+            "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
             "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
             "requires": {
                 "kind-of": "^3.0.2"
@@ -7717,7 +7736,7 @@
         },
         "load-json-file": {
             "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
             "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
             "requires": {
                 "graceful-fs": "^4.1.2",
@@ -8035,7 +8054,7 @@
         },
         "media-typer": {
             "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+            "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
             "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
         },
         "mem": {
@@ -8067,7 +8086,7 @@
         },
         "meow": {
             "version": "3.7.0",
-            "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+            "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
             "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
             "requires": {
                 "camelcase-keys": "^2.0.0",
@@ -8242,7 +8261,7 @@
         },
         "mkdirp": {
             "version": "0.5.1",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+            "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
             "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
             "requires": {
                 "minimist": "0.0.8"
@@ -8575,7 +8594,7 @@
         },
         "next-tick": {
             "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
             "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
         },
         "nice-try": {
@@ -8658,7 +8677,7 @@
                 },
                 "semver": {
                     "version": "5.3.0",
-                    "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+                    "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
                     "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
                 },
                 "tar": {
@@ -12258,7 +12277,7 @@
         },
         "os-homedir": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+            "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
             "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
         },
         "os-locale": {
@@ -12271,7 +12290,7 @@
         },
         "os-tmpdir": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+            "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
             "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
         },
         "osenv": {
@@ -12511,7 +12530,7 @@
         },
         "path-is-absolute": {
             "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
             "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
         },
         "path-is-inside": {
@@ -13906,7 +13925,7 @@
         },
         "readable-stream": {
             "version": "2.3.6",
-            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+            "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
             "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
             "requires": {
                 "core-util-is": "~1.0.0",
@@ -14348,7 +14367,7 @@
         },
         "safe-regex": {
             "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
             "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
             "requires": {
                 "ret": "~0.1.10"
@@ -14623,7 +14642,7 @@
         },
         "sha.js": {
             "version": "2.4.11",
-            "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+            "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
             "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
             "requires": {
                 "inherits": "^2.0.1",
@@ -15480,7 +15499,7 @@
         },
         "string_decoder": {
             "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+            "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
             "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
             "requires": {
                 "safe-buffer": "~5.1.0"
@@ -15510,7 +15529,7 @@
         },
         "strip-ansi": {
             "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
             "requires": {
                 "ansi-regex": "^2.0.0"
@@ -15526,7 +15545,7 @@
         },
         "strip-eof": {
             "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
             "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
         },
         "strip-indent": {
@@ -16298,7 +16317,7 @@
         },
         "tty-browserify": {
             "version": "0.0.0",
-            "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+            "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
             "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
             "dev": true
         },
@@ -17751,7 +17770,7 @@
         },
         "wrap-ansi": {
             "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+            "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
             "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
             "requires": {
                 "string-width": "^1.0.1",
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a9e81093a..401a4b15c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -493,7 +493,6 @@ export class MainView extends React.Component {
                     fieldKey={"data"}
                     annotationsKey={""}
                     select={emptyFunction}
-                    chromeCollapsed={true}
                     active={returnFalse}
                     isSelected={returnFalse}
                     moveDocument={this.moveButtonDoc}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e03e4aa04..c9c6af054 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -81,7 +81,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     toggleChrome = (): void => {
         this.props.docViews.map(dv => {
             const layout = Doc.Layout(dv.Document);
-            layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : "enabled");
+            layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : StrCast(layout._replacedChrome, "enabled"));
         });
     }
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index d71bad647..293a8491a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -33,7 +33,6 @@ export interface CollectionViewProps extends FieldViewProps {
     PanelWidth: () => number;
     PanelHeight: () => number;
     VisibleHeight?: () => number;
-    chromeCollapsed: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
     fieldKey: string;
 }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 1c4818957..8dd844711 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -115,7 +115,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
                 // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
                 // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
                 const chromeStatus = this.props.Document._chromeStatus;
-                if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
+                if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed" || chromeStatus === "replaced")) {
                     runInAction(() => this._collapsed = true);
                 }
             });
@@ -175,14 +175,14 @@ export class CollectionView extends Touchable<FieldViewProps> {
     }
 
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
-        const props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
+        const props = { ...this.props, ...renderProps, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
         switch (type) {
             case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />);
             case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
             case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />);
-            case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
-            case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
-            case CollectionViewType.Multirow: return (<CollectionMultirowView chromeCollapsed={true} key="rpwview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+            case CollectionViewType.Staff: return (<CollectionStaffView key="collview" {...props} />);
+            case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView key="collview" {...props} />);
+            case CollectionViewType.Multirow: return (<CollectionMultirowView key="rpwview" {...props} />);
             case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); }
             case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); }
             case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); }
@@ -201,7 +201,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         // 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
-        const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
+        const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) :
             <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
         return [chrome, this.SubViewHelper(type, renderProps)];
     }
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index ff7c4998f..ec05443e5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -258,6 +258,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
         return (
             <div className={"collectionMultirowView_contents"}
                 style={{
+                    width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`,
+                    height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`,
                     marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin),
                     marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin)
                 }} ref={this.createDashEventsTarget}>
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 881d80dda..09aaad9df 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1123,8 +1123,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                             CollectionView={undefined}
                             ScreenToLocalTransform={this.sidebarScreenToLocal}
                             renderDepth={this.props.renderDepth + 1}
-                            ContainingCollectionDoc={this.props.ContainingCollectionDoc}
-                            chromeCollapsed={true}>
+                            ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                         </CollectionFreeFormView>
                         <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} />
                     </div>}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 99c6fde34..db9800de4 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -436,8 +436,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 CollectionView={undefined}
                 ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                 renderDepth={this.props.renderDepth + 1}
-                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
-                chromeCollapsed={true}>
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                 {this.contentFunc}
             </CollectionFreeFormView>
         </div >);
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 2afdc190d..85017d3d4 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -40,6 +40,7 @@ export class PresBox extends React.Component<FieldViewProps> {
     componentDidMount() {
         const userDoc = CurrentUserUtils.UserDocument;
         this.props.Document._forceRenderEngine = "timeline";
+        this.props.Document._replacedChrome = "replaced";
         this._slideshowReaction = reaction(() => this.props.Document._viewType,
             (slideshow) => {
                 if (slideshow === CollectionViewType.Stacking || slideshow === undefined) {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index d12a8d151..6695e04c3 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -354,8 +354,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
                     CollectionView={undefined}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                     renderDepth={this.props.renderDepth + 1}
-                    ContainingCollectionDoc={this.props.ContainingCollectionDoc}
-                    chromeCollapsed={true}>
+                    ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                     {this.contentFunc}
                 </CollectionFreeFormView>
             </div>
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a48dc286e..7e49d957d 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -211,8 +211,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
                 CollectionView={undefined}
                 ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                 renderDepth={this.props.renderDepth + 1}
-                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
-                chromeCollapsed={true}>
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
                 {() => [this.content]}
             </CollectionFreeFormView>
         </div >);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index a7c1990e9..4f50be5b0 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -651,8 +651,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 CollectionView={undefined}
                 ScreenToLocalTransform={this.overlayTransform}
                 renderDepth={this.props.renderDepth + 1}
-                ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}
-                chromeCollapsed={true}>
+                ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}>
             </CollectionFreeFormView>
         </div>;
     }
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 1707524cb..aac6d4e3e 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -10,22 +10,24 @@ export const documentSchema = createSchema({
     title: "string",            // document title (can be on either data document or layout)
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
     childDropAction: "string",  // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
-    _nativeWidth: "number",      // native width of document which determines how much document contents are scaled when the document's width is set
-    _nativeHeight: "number",     // "
-    _width: "number",            // width of document in its container's coordinate system
-    _height: "number",           // "
+    _nativeWidth: "number",     // native width of document which determines how much document contents are scaled when the document's width is set
+    _nativeHeight: "number",    // "
+    _width: "number",           // width of document in its container's coordinate system
+    _height: "number",          // "
     _freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents
-    _LODdisable: "boolean",      // whether to disbale LOD switching for CollectionFreeFormViews
+    _LODdisable: "boolean",     // whether to disbale LOD switching for CollectionFreeFormViews
     _pivotField: "string",      // specifies which field should be used as the timeline/pivot axis
+    _replacedChrome: "string",  // what the default chrome is replaced with. Currently only supports the value of 'replaced' for PresBox's.
+    _chromeStatus: "string",    // determines the state of the collection chrome. values allowed are 'replaced', 'enabled', 'disabled', 'collapsed'
     color: "string",            // foreground color of document
     backgroundColor: "string",  // background color of document
     opacity: "number",          // opacity of document
-    creationDate: DateField,     // when the document was created
+    creationDate: DateField,    // when the document was created
     links: listSpec(Doc),       // computed (readonly) list of links associated with this document
     removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
-    onPointerDown: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
-    onPointerUp: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+    onPointerUp: ScriptField,   // script to run when document is clicked (can be overriden by an onClick prop)
     onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     ignoreAspect: "boolean",    // whether aspect ratio should be ignored when laying out or manipulating the document
@@ -36,7 +38,7 @@ export const documentSchema = createSchema({
     treeViewOpen: "boolean",    //  flag denoting whether the documents sub-tree (contents) is visible or hidden
     treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
     preventTreeViewOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
-    currentTimecode: "number",   // current play back time of a temporal document (video / audio)
+    currentTimecode: "number",  // current play back time of a temporal document (video / audio)
     maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) 
     lockedPosition: "boolean",  // whether the document can be moved (dragged)
     lockedTransform: "boolean", // whether the document can be panned/zoomed
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 3a2a0f513..0185017ec 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -48,7 +48,7 @@ export class CurrentUserUtils {
     // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
     static setupCreatorButtons(doc: Doc, buttons?: string[]) {
         const notes = CurrentUserUtils.setupNoteTypes(doc);
-        const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _chromeStatus: "enabled", showTitle: "title", boxShadow: "0 0" });
+        const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _chromeStatus: "replaced", showTitle: "title", boxShadow: "0 0" });
         const emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" });
         doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", _height: 75 });
         doc.activePen = doc;
@@ -270,7 +270,7 @@ export class CurrentUserUtils {
 
     // the initial presentation Doc to use
     static setupDefaultPresentation(doc: Doc) {
-        doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _chromeStatus: "enabled", showTitle: "title", boxShadow: "0 0" });
+        doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _chromeStatus: "replaced", showTitle: "title", boxShadow: "0 0" });
     }
 
     static setupMobileUploads(doc: Doc) {
-- 
cgit v1.2.3-70-g09d2


From ec5c878fb4c5f7e03fdd214c0841cf2ebf983e8c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 11 Feb 2020 15:48:06 -0500
Subject: refactored image upload data response format, google docs working for
 specific interactions

---
 .../util/Import & Export/DirectoryImportBox.tsx    |   4 +-
 src/client/util/RichTextSchema.tsx                 |   1 +
 src/client/views/collections/CollectionSubView.tsx |   8 +-
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 src/client/views/nodes/ImageBox.tsx                |   4 +-
 src/new_fields/RichTextUtils.ts                    |  30 ++---
 src/scraping/buxton/final/json/buxton.json         | 130 ++++++++++-----------
 src/server/ApiManagers/DownloadManager.ts          |   2 +-
 src/server/DashUploadUtils.ts                      |  37 ++++--
 src/server/apis/google/GooglePhotosUploadUtils.ts  |   3 +-
 10 files changed, 121 insertions(+), 102 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 071015193..d04f56e57 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -120,8 +120,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             runInAction(() => this.completed += batch.length);
         });
 
-        await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
-            const path = Utils.prepend(clientAccessPath);
+        await Promise.all(uploads.map(async ({ name, type, accessPaths, exifData }) => {
+            const path = Utils.prepend(accessPaths.agnostic.client);
             const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index f12b3632c..3cf0561dc 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -133,6 +133,7 @@ export const nodes: { [index: string]: NodeSpec } = {
         inline: true,
         attrs: {
             src: {},
+            agnostic: { default: null },
             width: { default: 100 },
             alt: { default: null },
             title: { default: null },
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 293a8491a..20941493f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -232,8 +232,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     const split = img.split("src=\"")[1].split("\"")[0];
                     let source = split;
                     if (split.startsWith("data:image") && split.includes("base64")) {
-                        const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [split] });
-                        source = Utils.prepend(clientAccessPath);
+                        const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [split] });
+                        source = Utils.prepend(accessPaths.agnostic.client);
                     }
                     const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
                     ImageUtils.ExtractExif(doc);
@@ -312,9 +312,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     const dropFileName = file ? file.name : "-empty-";
                     promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
                         results.map(action((result: any) => {
-                            const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result;
+                            const { accessPaths, nativeWidth, nativeHeight, contentSize } = result;
                             const full = { ...options, _width: 300, title: dropFileName };
-                            const pathname = Utils.prepend(clientAccessPath);
+                            const pathname = Utils.prepend(accessPaths.agnostic.client);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 if (doc) {
                                     const proto = Doc.GetProto(doc);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e4ab3e746..5c5a87cb9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -277,7 +277,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
             preventDefault && e.preventDefault();
         }
-    })
+    });
 
     buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
         const linkDocs = DocListCast(this.props.Document.links);
@@ -636,7 +636,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                         if (StrCast(tempDoc.title) === layout) {
                             foundLayout = tempDoc;
                         }
-                    })
+                    });
                 DocumentView.
                     makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout);
             } else {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index db9800de4..c0e102195 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -321,12 +321,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     const { dataDoc } = this;
                     const { success, failure, idle, loading } = uploadIcons;
                     runInAction(() => this.uploadIcon = loading);
-                    const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
+                    const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
                     dataDoc.originalUrl = primary;
                     let succeeded = true;
                     let data: ImageField | undefined;
                     try {
-                        data = new ImageField(Utils.prepend(clientAccessPath));
+                        data = new ImageField(Utils.prepend(accessPaths.agnostic.client));
                     } catch {
                         succeeded = false;
                     }
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 016bcc4ca..7c1fc39d8 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -114,6 +114,7 @@ export namespace RichTextUtils {
             width: number;
             title: string;
             url: string;
+            agnostic: string;
         }
 
         const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise<Map<string, ImageTemplate>> => {
@@ -135,17 +136,17 @@ export namespace RichTextUtils {
 
                 for (let i = 0; i < objects.length; i++) {
                     const object = objects[i];
-                    const { clientAccessPath } = uploads[i];
+                    const { accessPaths } = uploads[i];
+                    const { agnostic, _m } = accessPaths;
                     const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
                     const size = embeddedObject.size!;
                     const width = size.width!.magnitude!;
-                    const ext = extname(clientAccessPath);
-                    const url = Utils.prepend(clientAccessPath.replace(ext, "_m" + ext));
 
                     inlineObjectMap.set(object.objectId!, {
                         title: embeddedObject.title || `Imported Image from ${document.title}`,
                         width,
-                        url
+                        url: Utils.prepend(_m.client),
+                        agnostic: Utils.prepend(agnostic.client)
                     });
                 }
             }
@@ -267,19 +268,19 @@ export namespace RichTextUtils {
         };
 
         const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
-            const { url: src, width } = image;
+            const { url: src, width, agnostic } = image;
             let docid: string;
-            const guid = Utils.GenerateDeterministicGuid(src);
+            const guid = Utils.GenerateDeterministicGuid(agnostic);
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
-                const backingDoc = Docs.Create.ImageDocument(src, { _width: 300, _height: 300 });
+                const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
                 DocumentView.makeCustomViewClicked(backingDoc, undefined, Docs.Create.FreeformDocument);
                 docid = backingDoc[Id];
                 textNote[guid] = docid;
             } else {
                 docid = backingDocId;
             }
-            return schema.node("image", { src, width, docid, float: null, location: "onRight" });
+            return schema.node("image", { src, agnostic, width, docid, float: null, location: "onRight" });
         };
 
         const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
@@ -435,7 +436,7 @@ export namespace RichTextUtils {
                     const width = attrs.width;
                     requests.push(await EncodeImage({
                         startIndex: position + nodeSize - 1,
-                        uri: attrs.src,
+                        uri: attrs.agnostic,
                         width: Number(typeof width === "string" ? width.replace("px", "") : width)
                     }));
                 }
@@ -498,15 +499,18 @@ export namespace RichTextUtils {
             };
         };
 
-        const EncodeImage = async (information: ImageInformation) => {
-            const source = [Docs.Create.ImageDocument(information.uri)];
+        const EncodeImage = async ({ uri, width, startIndex }: ImageInformation) => {
+            if (!uri) {
+                return {};
+            }
+            const source = [Docs.Create.ImageDocument(uri)];
             const baseUrls = await GooglePhotos.Transactions.UploadThenFetch(source);
             if (baseUrls) {
                 return {
                     insertInlineImage: {
                         uri: baseUrls[0],
-                        objectSize: { width: { magnitude: information.width, unit: "PT" } },
-                        location: { index: information.startIndex }
+                        objectSize: { width: { magnitude: width, unit: "PT" } },
+                        location: { index: startIndex }
                     }
                 };
             }
diff --git a/src/scraping/buxton/final/json/buxton.json b/src/scraping/buxton/final/json/buxton.json
index 5c2e2c90c..16b28916b 100644
--- a/src/scraping/buxton/final/json/buxton.json
+++ b/src/scraping/buxton/final/json/buxton.json
@@ -18,9 +18,9 @@
             "3DCad_Brochure.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_19ce4ebd-f207-4724-b8bf-610c8e1cf322.png",
-            "/files/images/buxton/upload_4dde8de0-f1ec-4ffb-b7cc-57b8c4ae6c94.png",
-            "/files/images/buxton/upload_825e8ed8-063c-428b-917d-eb62ea218f05.png"
+            "/files/images/buxton/upload_793981b0-d7da-45d1-a643-596cc3834166.png",
+            "/files/images/buxton/upload_6259e28a-055e-4b02-b547-e72c5e6fa3c0.png",
+            "/files/images/buxton/upload_4f4fb80f-467a-41e7-9bf5-1b1f377b2ec4.png"
         ],
         "title": "3Dconnexion CadMan 3D Motion Controller",
         "company": "3Dconnexion",
@@ -63,11 +63,11 @@
             "SpaceMouse_Plus_Info_Sheet.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_f84cb54e-9bdb-4fbc-9503-6cfa8ddd65be.jpg",
-            "/files/images/buxton/upload_506b5361-a9fd-4440-a19b-5b4af461878a.png",
-            "/files/images/buxton/upload_60380383-3603-4624-98cf-50a0d4afe4c9.png",
-            "/files/images/buxton/upload_2fad6fb2-f1e1-4fe3-915b-33586f05fcad.png",
-            "/files/images/buxton/upload_effcd815-49eb-4cd4-82cf-2519e58eb729.png"
+            "/files/images/buxton/upload_4a6cbfa8-964b-41d3-9f24-7d1aed22cd79.jpg",
+            "/files/images/buxton/upload_8c33d572-07da-4599-944c-eb2370b16a63.png",
+            "/files/images/buxton/upload_9afc5cca-6208-4944-a0f6-1c3fcd41fdcf.png",
+            "/files/images/buxton/upload_74e79798-9c9e-4c54-b42f-5813f218bc63.png",
+            "/files/images/buxton/upload_2bea6876-dc4e-4681-a1d3-433caec163c1.png"
         ],
         "title": "3Dconnexion Magellan/SpaceMouse Plus",
         "company": "3Dconnexion",
@@ -114,13 +114,13 @@
             "SpaceBall_5000_Data_Sheet.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_81972990-8cad-4299-926a-fe5d49711a80.jpg",
-            "/files/images/buxton/upload_04103620-72e4-431f-b34f-61d0f158277a.png",
-            "/files/images/buxton/upload_9bfe7c5a-1c5e-46e3-8f8d-4e0eec3e38e3.png",
-            "/files/images/buxton/upload_77da85f5-6575-4a8d-acb6-52f2df3307b0.png",
-            "/files/images/buxton/upload_1c041a33-b258-42d1-b9f2-d610a4838fb3.jpg",
-            "/files/images/buxton/upload_841f0d98-27d7-4298-a71d-c5cba72a262a.jpg",
-            "/files/images/buxton/upload_0e0eaf99-f8bb-4f02-ad46-651cd67c13b1.jpg"
+            "/files/images/buxton/upload_94e39580-5cca-46e9-ae49-ab6cd2fe3cad.jpg",
+            "/files/images/buxton/upload_eacee8a5-9849-4401-9c38-aa46fc0f517b.png",
+            "/files/images/buxton/upload_9b0b1f35-ac7c-4160-8806-8d243223872b.png",
+            "/files/images/buxton/upload_cf0c6e39-9ae6-48ce-90bd-d9f335503439.png",
+            "/files/images/buxton/upload_b4285b9f-26cd-4de5-8e37-c4e563c3e7f6.jpg",
+            "/files/images/buxton/upload_5b6ff31b-81b2-4a74-8591-27730c24225b.jpg",
+            "/files/images/buxton/upload_64dcea06-3b35-4979-bf40-5dfa5d4668b1.jpg"
         ],
         "title": "3Dconnexion Spaceball 5000",
         "company": "3Dconnexion",
@@ -171,13 +171,13 @@
             "SpaceNavigator_Press_Release.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_b44ec511-334e-46c0-a315-63d90fac8117.jpg",
-            "/files/images/buxton/upload_734ae360-faaf-4c78-89b4-a3f120fe233e.png",
-            "/files/images/buxton/upload_d20bbf6a-7310-407a-8b85-c607e9cf7f73.png",
-            "/files/images/buxton/upload_e9236600-dd5f-4805-92ca-805d5a301aa1.jpg",
-            "/files/images/buxton/upload_c85c977d-ef99-44be-b55c-8e8dde124e83.png",
-            "/files/images/buxton/upload_a6034d66-7de6-4d4b-bd64-ea95cc00ae8a.png",
-            "/files/images/buxton/upload_07415a65-964a-4936-aa6e-79861e8ebcbf.png"
+            "/files/images/buxton/upload_73511beb-f2bf-4f28-a1ea-64ef52a00426.jpg",
+            "/files/images/buxton/upload_4e088fb4-e5ff-4a1b-ba88-5f4575c86fe3.png",
+            "/files/images/buxton/upload_876a9921-3eb1-4c35-99e3-b82924fb88c4.png",
+            "/files/images/buxton/upload_d23c2886-c2b9-4558-9bfb-966aca7be20e.jpg",
+            "/files/images/buxton/upload_aef9171b-2e19-4fcb-b2dc-2281e89d498b.png",
+            "/files/images/buxton/upload_bbdaf252-1689-4680-8b19-ec5e79088e44.png",
+            "/files/images/buxton/upload_4cf49440-cf6b-484d-8391-3a2b0fae2b7e.png"
         ],
         "title": "3Dconnexion SpaceNavigator ",
         "company": "3Dconnexion",
@@ -222,12 +222,12 @@
             "3M_2006_Catalogue_p18.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_52b83c2a-5fe4-42aa-a488-5667b0a5beae.jpg",
-            "/files/images/buxton/upload_e05d8710-d82b-4f3b-8940-3745db1c855c.jpg",
-            "/files/images/buxton/upload_557c6fc7-04a3-4698-acde-4e53ceb6793c.jpg",
-            "/files/images/buxton/upload_d9723beb-bfe3-4556-b17d-aedf2db59777.jpg",
-            "/files/images/buxton/upload_5cb15815-3cab-406f-bf95-f9410997b4f4.jpg",
-            "/files/images/buxton/upload_a7fc91e0-7672-4f81-a8f3-66072997bd44.jpg"
+            "/files/images/buxton/upload_02df5121-8eff-452c-a52b-a44238afc06d.jpg",
+            "/files/images/buxton/upload_169aa4ee-4c34-43a2-8302-48bb3b7a01b6.jpg",
+            "/files/images/buxton/upload_a4d11c27-e4fa-4912-96da-3d971ec49ca0.jpg",
+            "/files/images/buxton/upload_7e9e1862-72e7-4f0d-8451-36de8c1bbe9c.jpg",
+            "/files/images/buxton/upload_76fa705f-2470-439c-acf6-03482d7a9570.jpg",
+            "/files/images/buxton/upload_7b6dd2c9-b88f-4958-a4d0-94bb42172e26.jpg"
         ],
         "title": "3M EM500 Ergonomic Mouse",
         "company": "3M",
@@ -268,13 +268,13 @@
             "Abaton_ProPoint_Brochure.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_f6f038a9-3583-4d21-9a43-f762e1df0b52.jpg",
-            "/files/images/buxton/upload_ad5f2bbd-66be-4dde-afe4-60e353a63f74.jpg",
-            "/files/images/buxton/upload_2e384260-5d2b-47a4-9508-028462a5a1e7.jpg",
-            "/files/images/buxton/upload_4dfcd3af-f06d-4c3e-8dff-7c43acd79a16.jpg",
-            "/files/images/buxton/upload_19eea9af-abec-4617-8b8d-fcb6637420de.jpg",
-            "/files/images/buxton/upload_89702b8a-fcb5-426d-b5af-c58e62d1c8ff.jpg",
-            "/files/images/buxton/upload_44419996-994e-471c-8dc7-cda22b00ac24.jpg"
+            "/files/images/buxton/upload_37169968-d329-40c5-9483-6bc198edc43c.jpg",
+            "/files/images/buxton/upload_64731ea4-a3aa-43ec-a56d-d4f6ecba7cf0.jpg",
+            "/files/images/buxton/upload_c41feac8-3b98-4d85-b9c7-79b3482a0695.jpg",
+            "/files/images/buxton/upload_5f67807b-f61b-4816-b93f-6d88c95b2418.jpg",
+            "/files/images/buxton/upload_a3e3cab8-48ab-43c2-bed9-5cc0ce68928d.jpg",
+            "/files/images/buxton/upload_4c3ac379-6703-4c87-a4a0-ca450a1151d2.jpg",
+            "/files/images/buxton/upload_8df14dc0-d75d-41f0-9e70-a221598d3df3.jpg"
         ],
         "title": "Abaton ProPoint Optical Trackball",
         "company": "Abaton",
@@ -305,8 +305,8 @@
             "Active_Book_Brochure_p1.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_2159da7b-9ebd-41ed-8b60-9eb36a6c6685.jpg",
-            "/files/images/buxton/upload_6d470914-fc41-4151-858b-f945807e62a2.png"
+            "/files/images/buxton/upload_9c8aa6a2-570f-4915-b840-504c8531c341.jpg",
+            "/files/images/buxton/upload_fbb7ad12-2a5d-4f76-89a9-ec30d7260031.png"
         ],
         "title": "Active Book Company Active Book Prototype",
         "company": "Active Book Company",
@@ -342,11 +342,11 @@
             "Adesso_ACK-540PW_June 21_2003.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_189469a3-1133-4a4f-8720-19c04f29684e.jpg",
-            "/files/images/buxton/upload_73f5d066-f9b5-41ce-9ce3-e27253ef51e7.jpg",
-            "/files/images/buxton/upload_498e5cbd-d65e-4b01-bed2-bcb2723ffee5.jpg",
-            "/files/images/buxton/upload_97ee9d03-07e0-448c-89f6-6ac61948de68.jpg",
-            "/files/images/buxton/upload_661e7387-dcd7-4ca1-8493-2e94f8803ef7.jpg"
+            "/files/images/buxton/upload_967656ff-44d7-4693-bcb0-dc0eab204413.jpg",
+            "/files/images/buxton/upload_9ffd4ccb-8ca5-4926-8fa2-38277b2ccfb6.jpg",
+            "/files/images/buxton/upload_22399981-f8c0-46e0-953c-89f7eb043ad2.jpg",
+            "/files/images/buxton/upload_15ad02a9-123f-4e7e-94c5-d959076520f0.jpg",
+            "/files/images/buxton/upload_8905097a-3568-4d37-9ad3-126a0e429f12.jpg"
         ],
         "title": "Adesso ACK-540PB PS/2 Mini PS/2 Touchpad Keyboard",
         "company": "Adesso",
@@ -405,17 +405,17 @@
             "Adesso_KP_Mouse_11_Product.JPG"
         ],
         "__images": [
-            "/files/images/buxton/upload_0a127e30-9014-4300-bade-07945fba940f.jpg",
-            "/files/images/buxton/upload_ca20611c-9061-4e56-9d72-4c4a3c798d3f.jpg",
-            "/files/images/buxton/upload_12b46021-eeef-47f0-9e55-ca9e9b76f9e4.jpg",
-            "/files/images/buxton/upload_5e86ad07-77af-4d3e-a82e-eaa9cafdbbe4.jpg",
-            "/files/images/buxton/upload_4be64833-53e9-4216-b7ec-d521e0b87933.jpg",
-            "/files/images/buxton/upload_7cb731b5-5c52-47e0-a33b-c113530478f2.jpg",
-            "/files/images/buxton/upload_b20407b3-3578-4fa5-8f16-e0bf729e4daa.jpg",
-            "/files/images/buxton/upload_bee2fcb8-702a-46aa-9ec3-eb9b50ef1ba7.jpg",
-            "/files/images/buxton/upload_0ee0f6d5-34fc-41f3-9c8e-cf462dbcc40b.jpg",
-            "/files/images/buxton/upload_8f442582-b901-4a38-8df6-5a9b3195df9f.jpg",
-            "/files/images/buxton/upload_3f96763c-c3af-46a7-8831-3206c8bc460e.jpg"
+            "/files/images/buxton/upload_5bc99192-0935-4f6d-8d6c-a2218d55b359.jpg",
+            "/files/images/buxton/upload_52a6212e-c79d-4ec6-86a0-a3ca4ea54387.jpg",
+            "/files/images/buxton/upload_997acb64-0e08-4af2-b0e3-0b86a18bace5.jpg",
+            "/files/images/buxton/upload_00586980-0a08-43e1-9721-c2f5dcd1079e.jpg",
+            "/files/images/buxton/upload_3f695b11-dc1d-4d54-abec-eacfac80bdc7.jpg",
+            "/files/images/buxton/upload_161f1420-9594-4dca-ba62-1ca32e9a2028.jpg",
+            "/files/images/buxton/upload_a8b4ea6e-f1f8-47db-a796-72f566e60e6f.jpg",
+            "/files/images/buxton/upload_b6365e4e-4656-4bbf-aec6-feb920be6df9.jpg",
+            "/files/images/buxton/upload_1a0e9061-39e1-4dec-8add-5cc1abd110b5.jpg",
+            "/files/images/buxton/upload_d9bba544-f2b1-4a15-81e5-0d10e60d7881.jpg",
+            "/files/images/buxton/upload_56d486df-d7bb-44b0-80ad-c41c63590bae.jpg"
         ],
         "title": "Adesso 2-in-1 Optical Keypad Calculator Mouse AKP-170",
         "company": "Adesso Inc",
@@ -477,18 +477,18 @@
             "NB75D_Mouse_Manual.jpg"
         ],
         "__images": [
-            "/files/images/buxton/upload_dae8f950-38bf-4fdf-8db0-153e3cae9cae.jpg",
-            "/files/images/buxton/upload_3e054d83-d01f-4d3d-aad2-fa8ee1d22a97.jpg",
-            "/files/images/buxton/upload_e9c2ddb0-0684-4bb4-a88c-5ee8074e456e.jpg",
-            "/files/images/buxton/upload_9c829a69-7d45-4d25-b906-98153aa03001.jpg",
-            "/files/images/buxton/upload_4faa425c-a100-4ce4-869c-a6c6705779ee.jpg",
-            "/files/images/buxton/upload_ed20ac31-69f6-45b8-b8e0-b8ad829ba002.jpg",
-            "/files/images/buxton/upload_f3d3d8f5-01cf-4cd1-9d1c-0a11a8c3e239.jpg",
-            "/files/images/buxton/upload_af1fa3b6-4537-416a-b626-34bed1154f35.jpg",
-            "/files/images/buxton/upload_5127f377-b697-4ad7-9fb3-fd6f82017c3c.jpg",
-            "/files/images/buxton/upload_061ae6a2-e083-492a-bc8a-826c4be00790.jpg",
-            "/files/images/buxton/upload_32e91966-7f1b-4cd7-858b-9cdb63affd13.jpg",
-            "/files/images/buxton/upload_ccc9de21-b047-4f9c-89c6-08fb3d206c7a.jpg"
+            "/files/images/buxton/upload_2db5c520-b370-4f03-8481-908e42428466.jpg",
+            "/files/images/buxton/upload_b28643eb-57a0-4602-b7cc-c41d553628b9.jpg",
+            "/files/images/buxton/upload_2478a89a-4eea-4e4c-af83-ecd292f44f3d.jpg",
+            "/files/images/buxton/upload_113d7356-f550-4491-849d-f69087c73918.jpg",
+            "/files/images/buxton/upload_44c40c44-ade6-499d-951c-4b436131e66e.jpg",
+            "/files/images/buxton/upload_d91f9756-09bc-447f-8f04-dc78344a5f11.jpg",
+            "/files/images/buxton/upload_b8c51011-5f93-43f3-85aa-b611a3b3cf93.jpg",
+            "/files/images/buxton/upload_483544a3-c3f7-4c8f-a236-def10d32f4f0.jpg",
+            "/files/images/buxton/upload_ea048294-3f57-42c3-b5b4-27062a5755dc.jpg",
+            "/files/images/buxton/upload_8eb1b9b1-9ca3-49c8-9bd4-7302a9c1f07e.jpg",
+            "/files/images/buxton/upload_029aa1d6-056d-4cf0-8eaf-b23b3d0e4317.jpg",
+            "/files/images/buxton/upload_3acc0955-da14-4a53-ac58-2e494e04ca78.jpg"
         ],
         "title": "A4 Tech BatteryFREE Wireless Optical Mouse Model NB-75D",
         "company": "A4Tech",
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index fad5e6789..01d2dfcad 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -254,7 +254,7 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera
                 // and dropped in the browser and thus hosted remotely) so we upload it
                 // to our server and point the zip file to it, so it can bundle up the bytes
                 const information = await DashUploadUtils.UploadImage(result);
-                path = information instanceof Error ? "" : information.serverAccessPaths[SizeSuffix.Original];
+                path = information instanceof Error ? "" : information.accessPaths[SizeSuffix.Original].server;
             }
             // write the file specified by the path to the directory in the
             // zip file given by the prefix.
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 83a0064e8..0f1758c26 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -61,13 +61,16 @@ export namespace DashUploadUtils {
     const type = "content-type";
 
     export interface ImageUploadInformation {
-        clientAccessPath: string;
-        serverAccessPaths: { [key: string]: string };
+        accessPaths: AccessPathInfo;
         exifData: EnrichedExifData;
         contentSize?: number;
         contentType?: string;
     }
 
+    export interface AccessPathInfo {
+        [suffix: string]: { client: string, server: string };
+    }
+
     const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
 
     export async function upload(file: File): Promise<any> {
@@ -94,7 +97,7 @@ export namespace DashUploadUtils {
         }
 
         console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
-        return { clientAccessPath: undefined };
+        return { accessPaths: undefined };
     }
 
     async function UploadPdf(absolutePath: string) {
@@ -213,29 +216,40 @@ export namespace DashUploadUtils {
         };
     };
 
-    export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<{ clientAccessPath: Opt<string> }> {
-        return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
+    export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<Opt<AccessPathInfo>> {
+        return new Promise<Opt<AccessPathInfo>>(resolve => {
             const filename = basename(absolutePath);
             const destinationPath = serverPathToFile(destination, filename);
             rename(absolutePath, destinationPath, error => {
-                resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
+                resolve(error ? undefined : {
+                    agnostic: getAccessPaths(destination, filename)
+                });
             });
         });
     }
 
+    function getAccessPaths(directory: Directory, fileName: string) {
+        return {
+            client: clientPathToFile(directory, fileName),
+            server: serverPathToFile(directory, fileName)
+        };
+    }
+
     export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<ImageUploadInformation> => {
         const { requestable, source, ...remaining } = metadata;
         const extension = `.${remaining.contentType.split("/")[1].toLowerCase()}`;
         const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${extension}`;
+        const { images } = Directory;
         const information: ImageUploadInformation = {
-            clientAccessPath: clientPathToFile(Directory.images, resolved),
-            serverAccessPaths: {},
+            accessPaths: {
+                agnostic: getAccessPaths(images, resolved)
+            },
             ...remaining
         };
         const outputPath = pathToDirectory(Directory.images);
         const writtenFiles = await outputResizedImages(() => request(requestable), outputPath, resolved, extension);
         for (const suffix of Object.keys(writtenFiles)) {
-            information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, writtenFiles[suffix]);
+            information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
         }
         if (isLocal().test(source) && cleanUp) {
             unlinkSync(source);
@@ -270,10 +284,9 @@ export namespace DashUploadUtils {
     export async function outputResizedImages(readStreamSource: () => ReadStreamLike | Promise<ReadStreamLike>, outputPath: string, fileName: string, ext: string) {
         const writtenFiles: { [suffix: string]: string } = {};
         for (const { resizer, suffix } of resizers(ext)) {
-            const resizedPath = path.resolve(outputPath, InjectSize(fileName, suffix));
-            writtenFiles[suffix] = resizedPath;
+            const resolved = writtenFiles[suffix] = InjectSize(fileName, suffix);
             await new Promise<void>(async (resolve, reject) => {
-                const writeStream = createWriteStream(resizedPath);
+                const writeStream = createWriteStream(path.resolve(outputPath, resolved));
                 let readStream: ReadStreamLike;
                 const source = readStreamSource();
                 if (source instanceof Promise) {
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 8ae63caa3..d305eed0a 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -84,6 +84,7 @@ export namespace GooglePhotosUploadUtils {
         if (!DashUploadUtils.validateExtension(url)) {
             return undefined;
         }
+        const body = await request(url, { encoding: null }); // returns a readable stream with the unencoded binary image data
         const parameters = {
             method: 'POST',
             uri: prepend('uploads'),
@@ -92,7 +93,7 @@ export namespace GooglePhotosUploadUtils {
                 'X-Goog-Upload-File-Name': filename || path.basename(url),
                 'X-Goog-Upload-Protocol': 'raw'
             },
-            body: await request(url, { encoding: null }) // returns a readable stream with the unencoded binary image data
+            body
         };
         return new Promise((resolve, reject) => request(parameters, (error, _response, body) => {
             if (error) {
-- 
cgit v1.2.3-70-g09d2


From 553fb2a11aa638c35192a1a2f7da99729867e542 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 13 Feb 2020 22:30:32 -0500
Subject: starting a broad cleanup of code.  trying to make standard doc field
 names be more regular

---
 src/client/documents/Documents.ts                  | 15 ++--
 src/client/util/DragManager.ts                     |  1 -
 src/client/util/RichTextRules.ts                   |  3 +-
 src/client/views/MainView.tsx                      |  7 +-
 .../views/collections/CollectionCarouselView.scss  |  1 +
 .../views/collections/CollectionCarouselView.tsx   |  1 -
 .../views/collections/CollectionLinearView.tsx     | 10 +--
 src/client/views/collections/CollectionSubView.tsx |  5 +-
 .../views/collections/CollectionTreeView.tsx       | 35 ++++-----
 src/client/views/collections/CollectionView.tsx    |  5 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  3 +-
 .../views/nodes/ContentFittingDocumentView.tsx     | 24 +-----
 src/client/views/nodes/DocumentView.tsx            | 36 ++-------
 src/client/views/nodes/FormattedTextBox.tsx        | 11 ---
 src/client/views/nodes/ImageBox.tsx                |  9 ++-
 src/new_fields/Doc.ts                              | 90 +++++++++-------------
 src/new_fields/documentSchemas.ts                  | 12 ++-
 .../authentication/models/current_user_utils.ts    |  8 +-
 18 files changed, 96 insertions(+), 180 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8304c2a7f..adcdf260d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -22,7 +22,6 @@ import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField }
 import { HtmlField } from "../../new_fields/HtmlField";
 import { List } from "../../new_fields/List";
 import { Cast, NumCast } from "../../new_fields/Types";
-import { IconField } from "../../new_fields/IconField";
 import { listSpec } from "../../new_fields/Schema";
 import { DocServer } from "../DocServer";
 import { dropActionType } from "../util/DragManager";
@@ -53,7 +52,6 @@ import { InkingStroke } from "../views/InkingStroke";
 import { InkField } from "../../new_fields/InkField";
 import { InkingControl } from "../views/InkingControl";
 import { RichTextField } from "../../new_fields/RichTextField";
-import { Networking } from "../Network";
 import { extname } from "path";
 import { MessageStore } from "../../server/Message";
 const requestImageSize = require('../util/request-image-size');
@@ -92,7 +90,6 @@ export interface DocumentOptions {
     scale?: number;
     isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
     forceActive?: boolean;
-    preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
     layout?: string | Doc;
     hideHeadings?: boolean; // whether stacking view column headings should be hidden
     isTemplateForField?: string; // the field key for which the containing document is a rendering template
@@ -109,7 +106,6 @@ export interface DocumentOptions {
     curPage?: number;
     currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)  value is in seconds
     displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
-    documentText?: string;
     borderRounding?: string;
     boxShadow?: string;
     sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
@@ -124,15 +120,16 @@ export interface DocumentOptions {
     onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
     onPointerDown?: ScriptField;
     onPointerUp?: ScriptField;
+    dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
     dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
     onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
-    clipboard?: Doc; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
+    clipboard?: Doc;
     icon?: string;
     sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
     targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
-    dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
     strokeWidth?: number;
     color?: string;
+    treeViewPreventOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expand/collapse state to be independent of other views of the same document in the tree view
     treeViewHideTitle?: boolean; // whether to hide the title of a tree view
     treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
     treeViewOpen?: boolean; // whether this document is expanded in a tree view
@@ -141,9 +138,9 @@ export interface DocumentOptions {
     limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
     // [key: string]: Opt<Field>;
     pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
-    isExpanded?: boolean; // is linear view expanded
-    textTransform?: string; // is linear view expanded
-    letterSpacing?: string; // is linear view expanded
+    linearViewIsExpanded?: boolean; // is linear view expanded
+    textTransform?: string;
+    letterSpacing?: string;
 }
 
 class EmptyBox {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index e572f0fcb..2877d5fd7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -134,7 +134,6 @@ export namespace DragManager {
         embedDoc?: boolean;
         moveDocument?: MoveFunction;
         isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged.  this will suppress onDragStart scripts
-        applyAsTemplate?: boolean;
     }
     export class LinkDragData {
         constructor(linkSourceDoc: Doc) {
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 6b8762a5e..de0f46202 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -121,8 +121,7 @@ export class RichTextRules {
             new InputRule(
                 new RegExp(/##$/),
                 (state, match, start, end) => {
-                    const schemaDoc = Doc.GetDataDoc(this.Document);
-                    const textDoc = Doc.GetProto(Cast(schemaDoc[DataSym], Doc, null)!);
+                    const textDoc = this.Document[DataSym];
                     const numInlines = NumCast(textDoc.inlineTextCount);
                     textDoc.inlineTextCount = numInlines + 1;
                     const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 401a4b15c..192f3b8fb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -482,12 +482,13 @@ export class MainView extends React.Component {
         return new Transform(-translateX, -translateY, 1 / scale);
     }
     @computed get docButtons() {
-        if (CurrentUserUtils.UserDocument?.expandingButtons instanceof Doc) {
+        const expandingBtns = Doc.UserDoc()?.expandingButtons;
+        if (expandingBtns instanceof Doc) {
             return <div className="mainView-docButtons" ref={this._docBtnRef}
-                style={{ height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} >
+                style={{ height: !expandingBtns.linearViewIsExpanded ? "42px" : undefined }} >
                 <MainViewNotifs />
                 <CollectionLinearView
-                    Document={CurrentUserUtils.UserDocument.expandingButtons}
+                    Document={expandingBtns}
                     DataDoc={undefined}
                     LibraryPath={emptyPath}
                     fieldKey={"data"}
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index ad369bbff..fd1296286 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -1,6 +1,7 @@
 
 .collectionCarouselView-outer {
     background: gray;
+    height : 100%;
     .collectionCarouselView-caption {
         margin-left: 10%;
         margin-right: 10%;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index f462e20b4..3c86bd799 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -12,7 +12,6 @@ import { CollectionSubView } from './CollectionSubView';
 import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
 import { Doc } from '../../../new_fields/Doc';
 import { FormattedTextBox } from '../nodes/FormattedTextBox';
-import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { ObjectField } from '../../../new_fields/ObjectField';
 
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 67062ae41..7eb316cf0 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -38,8 +38,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
 
     componentDidMount() {
         // is there any reason this needs to exist? -syip.  yes, it handles autoHeight for stacking views (masonry isn't yet supported).
-        this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
-            () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+        this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.linearViewIsExpanded ? 1 : 0),
+            () => this.props.Document._width = 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
             { fireImmediately: true }
         );
 
@@ -84,8 +84,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
         const guid = Utils.GenerateGuid();
         return <div className="collectionLinearView-outer">
             <div className="collectionLinearView" ref={this.createDashEventsTarget} >
-                <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
-                    onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
+                <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle}
+                    onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} />
                 <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
 
                 <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25) }}>
@@ -97,7 +97,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                         return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
                             style={{
                                 width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize,
-                                height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize,
+                                height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize,
                             }}  >
                             <DocumentView
                                 Document={pair.layout}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 343be1b58..0963e1ea6 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -159,7 +159,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             const docDragData = de.complete.docDragData;
             (this.props.Document.dropConverter instanceof ScriptField) &&
                 this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this 
-            if (docDragData && !docDragData.applyAsTemplate) {
+            if (docDragData) {
                 if (de.altKey && docDragData.draggedDocuments.length) {
                     this.childDocs.map(doc => {
                         doc.layout_fromParent = docDragData.draggedDocuments[0];
@@ -253,7 +253,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                             }
                         });
                     } else {
-                        const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text });
+                        const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300 });
+                        Doc.GetProto(htmlDoc)["data-text"] = text;
                         this.props.addDocument(htmlDoc);
                     }
                     return;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index e91c3cc49..a160dcc62 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -61,7 +61,7 @@ export interface TreeViewProps {
     parentKey: string;
     active: (outsideReaction?: boolean) => boolean;
     treeViewHideHeaderFields: () => boolean;
-    preventTreeViewOpen: boolean;
+    treeViewPreventOpen: boolean;
     renderedIds: string[];
     onCheckedClick?: ScriptField;
 }
@@ -84,7 +84,7 @@ library.add(faPlus, faMinus);
  * 
  * special fields:
  * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden
- * preventTreeViewOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+ * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
  * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree
  */
 class TreeView extends React.Component<TreeViewProps> {
@@ -96,8 +96,8 @@ class TreeView extends React.Component<TreeViewProps> {
 
     get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); }
     @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
-    set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; }
-    @computed get treeViewOpen() { return (!this.props.preventTreeViewOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
+    set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; }
+    @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
     @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }
     @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
     @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; }
@@ -179,8 +179,7 @@ class TreeView extends React.Component<TreeViewProps> {
         SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)}
         OnFillDown={undoBatch((value: string) => {
             Doc.SetInPlace(this.props.document, key, value, false);
-            const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined;
-            const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
+            const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
             EditableView.loadId = doc[Id];
             return this.props.addDocument(doc);
         })}
@@ -291,7 +290,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
                     DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
                     this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
-                    this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.preventTreeViewOpen,
+                    this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen,
                     [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick);
             } else {
                 contentElement = <EditableView
@@ -334,7 +333,7 @@ class TreeView extends React.Component<TreeViewProps> {
                     TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document),
                         this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
                         this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
-                        this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.preventTreeViewOpen,
+                        this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen,
                         [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)}
             </ul >;
         } else if (this.treeViewExpandedView === "fields") {
@@ -464,7 +463,7 @@ class TreeView extends React.Component<TreeViewProps> {
         ChromeHeight: undefined | (() => number),
         renderDepth: number,
         treeViewHideHeaderFields: () => boolean,
-        preventTreeViewOpen: boolean,
+        treeViewPreventOpen: boolean,
         renderedIds: string[],
         libraryPath: Doc[] | undefined,
         onCheckedClick: ScriptField | undefined
@@ -574,7 +573,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 parentKey={key}
                 active={active}
                 treeViewHideHeaderFields={treeViewHideHeaderFields}
-                preventTreeViewOpen={preventTreeViewOpen}
+                treeViewPreventOpen={treeViewPreventOpen}
                 renderedIds={renderedIds} />;
         });
     }
@@ -601,8 +600,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
 
     @action
     remove = (document: Document): boolean => {
-        const targetDataDoc = this.props.Document && !this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document[DataSym] as Doc);
-        const children = Cast(targetDataDoc[this.props.fieldKey], listSpec(Doc), []);
+        const children = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []);
         if (children.indexOf(document) !== -1) {
             children.splice(children.indexOf(document), 1);
             return true;
@@ -611,10 +609,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
     @action
     addDoc = (doc: Document, relativeTo: Opt<Doc>, before?: boolean): boolean => {
-        const doAddDoc = () => {
-            const targetDataDoc = this.props.Document && !this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document[DataSym] as Doc);
-            Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc, relativeTo, before, false, false, false);
-        };
+        const doAddDoc = () =>
+            Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false);
         if (this.props.Document.resolvedDataDoc instanceof Promise) {
             this.props.Document.resolvedDataDoc.then(resolved => doAddDoc());
         } else {
@@ -637,7 +633,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
         } else {
             const layoutItems: ContextMenuProps[] = [];
-            layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" });
+            layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" });
             layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" });
             layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" });
             ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
@@ -733,8 +729,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                     SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
                     OnFillDown={undoBatch((value: string) => {
                         Doc.SetInPlace(this.dataDoc, "title", value, false);
-                        const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined;
-                        const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
+                        const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
                         EditableView.loadId = doc[Id];
                         this.addDoc(doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
                     })} />)}
@@ -744,7 +739,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                         TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
                             moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
                             this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.treeViewHideHeaderFields),
-                            BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick))
+                            BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick))
                     }
                 </ul>
             </div >
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index ad39b69d8..39ce4d6e8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -133,7 +133,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     @action.bound
     addDocument(doc: Doc): boolean {
-        const targetDataDoc = this.props.Document.resolvedDataDoc && !this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document[DataSym]);
+        const targetDataDoc = this.props.Document[DataSym];
         targetDataDoc[this.props.fieldKey] = new List<Doc>([...DocListCast(targetDataDoc[this.props.fieldKey]), doc]);  // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there
         // Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
         targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
@@ -145,8 +145,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     removeDocument(doc: Doc): boolean {
         const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
         docView && SelectionManager.DeselectDoc(docView);
-        const targetDataDoc = this.props.Document.resolvedDataDoc && !this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document[DataSym]);
-        const value = Cast(targetDataDoc[this.props.fieldKey], listSpec(Doc), []);
+        const value = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []);
         let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
         index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7f1817a15..e1854fc2d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1074,7 +1074,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
         // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
         // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
-        // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
         return <div className={"collectionfreeformview-container"}
             ref={this.createDashEventsTarget}
             onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
@@ -1086,7 +1085,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 width: this.contentScaling ? `${100 / this.contentScaling}%` : "",
                 height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
             }}>
-            {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
+            {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
                 this.placeholder : this.marqueeView}
             <CollectionFreeFormOverlayView elements={this.elementFunc} />
         </div>;
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 671f5b96e..5c449026e 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -1,19 +1,17 @@
 import React = require("react");
-import { action, computed } from "mobx";
+import { computed } from "mobx";
 import { observer } from "mobx-react";
 import "react-table/react-table.css";
 import { Doc, Opt } from "../../../new_fields/Doc";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { ScriptField } from "../../../new_fields/ScriptField";
 import { NumCast, StrCast } from "../../../new_fields/Types";
+import { TraceMobx } from "../../../new_fields/util";
 import { emptyFunction, returnEmptyString, returnOne } from "../../../Utils";
-import { DragManager } from "../../util/DragManager";
 import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
+import { CollectionView } from "../collections/CollectionView";
 import '../DocumentDecorations.scss';
 import { DocumentView } from "../nodes/DocumentView";
 import "./ContentFittingDocumentView.scss";
-import { CollectionView } from "../collections/CollectionView";
-import { TraceMobx } from "../../../new_fields/util";
 
 interface ContentFittingDocumentViewProps {
     Document?: Doc;
@@ -55,20 +53,6 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
     }
     private contentScaling = () => this.scaling;
 
-    @undoBatch
-    @action
-    drop = (e: Event, de: DragManager.DropEvent) => {
-        const docDragData = de.complete.docDragData;
-        if (docDragData) {
-            this.props.childDocs && this.props.childDocs.map(otherdoc => {
-                const target = Doc.GetProto(otherdoc);
-                target.layout = ComputedField.MakeFunction("this.image_data[0]");
-                target.layout_custom = Doc.MakeDelegate(docDragData.draggedDocuments[0]);
-            });
-            e.stopPropagation();
-        }
-        return true;
-    }
     private PanelWidth = () => this.panelWidth;
     private PanelHeight = () => this.panelHeight;;
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 48df1ea33..e9b4f9f7f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -92,7 +92,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _downY: number = 0;
     private _lastTap: number = 0;
     private _doubleTap = false;
-    private _hitTemplateDrag = false;
     private _mainCont = React.createRef<HTMLDivElement>();
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
@@ -196,14 +195,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
     }
 
-    startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) {
+    startDragging(x: number, y: number, dropAction: dropActionType) {
         if (this._mainCont.current) {
             const dragData = new DragManager.DocumentDragData([this.props.Document]);
             const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
             dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
             dragData.dropAction = dropAction;
             dragData.moveDocument = this.props.moveDocument;//  this.Document.onDragStart ? undefined : this.props.moveDocument;
-            dragData.applyAsTemplate = applyAsTemplate;
             dragData.dragDivName = this.props.dragDivName;
             this.props.Document.sourceContext = this.props.ContainingCollectionDoc; // bcz: !! shouldn't need this ... use search find the document's context dynamically
             DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart });
@@ -257,8 +255,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             let preventDefault = true;
             if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
                 const fullScreenAlias = Doc.MakeAlias(this.props.Document);
-                if (StrCast(fullScreenAlias.layoutKey) !== "layout_custom" && fullScreenAlias.layout_custom !== undefined) {
-                    fullScreenAlias.layoutKey = "layout_custom";
+                if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
+                    fullScreenAlias.layoutKey = "layout_fullScreen";
                 }
                 this.props.addDocTab(fullScreenAlias, undefined, "inTab");
                 SelectionManager.DeselectAll();
@@ -323,7 +321,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
                 if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
                     this.cleanUpInteractions();
-                    this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+                    this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
                 }
             }
             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
@@ -434,14 +432,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) {
             this._downX = e.clientX;
             this._downY = e.clientY;
-            this._hitTemplateDrag = false;
-            // this whole section needs to move somewhere else.  We're trying to initiate a special "template" drag where
-            // this document is the template and we apply it to whatever we drop it on.
-            for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
-                if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
-                    this._hitTemplateDrag = true;
-                }
-            }
             if ((this.active || this.Document.onDragStart || this.onClickHandler) &&
                 !e.ctrlKey &&
                 (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
@@ -470,7 +460,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
                     document.removeEventListener("pointermove", this.onPointerMove);
                     document.removeEventListener("pointerup", this.onPointerUp);
-                    this.startDragging(this._downX, this._downY, this.props.ContainingCollectionDoc?.childDropAction ? this.props.ContainingCollectionDoc?.childDropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+                    this.startDragging(this._downX, this._downY, this.props.ContainingCollectionDoc?.childDropAction ? this.props.ContainingCollectionDoc?.childDropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
                 }
             }
             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
@@ -556,17 +546,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
 
-    @undoBatch
-    makeSelBtnClicked = (): void => {
-        if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) {
-            this.Document.isButton = false;
-            this.Document.ignoreClick = false;
-            this.Document.onClick = undefined;
-        } else {
-            this.props.Document.isButton = "Selector";
-        }
-    }
-
     @undoBatch
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
@@ -578,10 +557,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc },
                 `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`);
         }
-        if (de.complete.docDragData && de.complete.docDragData.applyAsTemplate) {
-            Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom", undefined);
-            e.stopPropagation();
-        }
         if (de.complete.linkDragData) {
             e.stopPropagation();
             // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
@@ -697,7 +672,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(this, "${this.props.Document.layoutKey}")`), icon: "window-restore" });
         onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
         onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
-        onClicks.push({ description: this.props.Document.isButton ? "Remove Select Link Behavior" : "Select Link", event: this.makeSelBtnClicked, icon: "concierge-bell" });
         onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) });
         !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 7fbee8881..2fb61f9db 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -250,17 +250,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                     Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text);
                     e.stopPropagation();
                 }
-                // apply as template when dragging with Meta
-            } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.metaKey) {
-                draggedDoc.isTemplateDoc = true;
-                let newLayout = Doc.Layout(draggedDoc);
-                if (typeof (draggedDoc.layout) === "string") {
-                    newLayout = Doc.MakeDelegate(draggedDoc);
-                    newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`);
-                }
-                this.Document.layout_custom = newLayout;
-                this.Document.layoutKey = "layout_custom";
-                e.stopPropagation();
                 // embed document when dragging with a userDropAction or an embedDoc flag set
             } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) {
                 const target = de.complete.docDragData.droppedDocuments[0];
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c0e102195..364bce7a8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -92,10 +92,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) {
                 const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
                 const targetField = Doc.LayoutFieldKey(layoutDoc);
-                if (layoutDoc?.[DataSym][targetField] instanceof ImageField) {
-                    this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(layoutDoc[DataSym][targetField] as ImageField);
-                    this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(layoutDoc[DataSym][targetField + "-nativeWidth"]);
-                    this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(layoutDoc[DataSym][targetField + "-nativeHeight"]);
+                const targetDoc = layoutDoc[DataSym];
+                if (targetDoc[targetField] instanceof ImageField) {
+                    this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
+                    this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]);
+                    this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(targeDoc[targetField + "-nativeHeight"]);
                     e.stopPropagation();
                 }
             }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 2305f2dce..55c0660c0 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -17,7 +17,6 @@ import { intersectRect } from "../Utils";
 import { UndoManager, undoBatch } from "../client/util/UndoManager";
 import { computedFn } from "mobx-utils";
 import { RichTextField } from "./RichTextField";
-import { Script } from "vm";
 
 export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
@@ -112,7 +111,12 @@ export class Doc extends RefField {
             get: getter,
             // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
             has: (target, key) => key in target.__fields,
-            ownKeys: target => Object.keys(target.__allfields),
+            ownKeys: target => {
+                let obj = {} as any;
+                Object.assign(obj, target.___fields);
+                runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
+                return Object.keys(obj)
+            },
             getOwnPropertyDescriptor: (target, prop) => {
                 if (prop.toString() === "__LAYOUT__") {
                     return Reflect.getOwnPropertyDescriptor(target, prop);
@@ -140,17 +144,7 @@ export class Doc extends RefField {
     [key: string]: FieldResult;
 
     @serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize })))
-    private get __fields() {
-        return this.___fields;
-    }
-    private get __allfields() {
-        let obj = {} as any;
-        Object.assign(obj, this.___fields);
-        runInAction(() => obj.__LAYOUT__ = this.__LAYOUT__);
-        return obj;
-    }
-
-
+    private get __fields() { return this.___fields; }
     private set __fields(value) {
         this.___fields = value;
         for (const key in value) {
@@ -168,18 +162,19 @@ export class Doc extends RefField {
     private [UpdatingFromServer]: boolean = false;
 
     private [Update] = (diff: any) => {
-        if (this[UpdatingFromServer]) {
-            return;
-        }
-        DocServer.UpdateField(this[Id], diff);
+        !this[UpdatingFromServer] && DocServer.UpdateField(this[Id], diff);
     }
 
     private [Self] = this;
     private [SelfProxy]: any;
     public [WidthSym] = () => NumCast(this[SelfProxy]._width);
     public [HeightSym] = () => NumCast(this[SelfProxy]._height);
-    public get [DataSym]() { return Cast(Doc.Layout(this[SelfProxy]).resolvedDataDoc, Doc, null) || this[SelfProxy]; }
     public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
+    public get [DataSym]() {
+        const self = this[SelfProxy];
+        return self.resolvedDataDoc && !self.isTemplateForField ? self :
+            Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self);
+    }
     @computed get __LAYOUT__() {
         const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
         if (templateLayoutDoc) {
@@ -195,12 +190,8 @@ export class Doc extends RefField {
         return undefined;
     }
 
-    [ToScriptString]() {
-        return "invalid";
-    }
-    [ToString]() {
-        return "Doc";
-    }
+    [ToScriptString]() { return "invalid"; }
+    [ToString]() { return "Doc"; }
 
     private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
     public static CurrentUserEmail: string = "";
@@ -287,8 +278,7 @@ export namespace Doc {
 
     export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
         try {
-            const self = doc[Self];
-            return getField(self, key, ignoreProto);
+            return getField(doc[Self], key, ignoreProto);
         } catch  {
             return doc;
         }
@@ -357,13 +347,12 @@ export namespace Doc {
         return r || r2 || r3 || r4;
     }
 
-    // gets the document's prototype or returns the document if it is a prototype
-    export function GetProto(doc: Doc) {
-        return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
-    }
-    export function GetDataDoc(doc: Doc): Doc {
-        const proto = Doc.GetProto(doc);
-        return proto === doc ? proto : Doc.GetDataDoc(proto);
+    // Gets the data document for the document.  Note: this is mis-named -- it does not specifically
+    // return the doc's proto, but rather recursively searches through the proto inheritance chain 
+    // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
+    export function GetProto(doc: Doc): Doc {
+        const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
+        return proto === doc ? proto : Doc.GetProto(proto);
     }
 
     export function allKeys(doc: Doc): string[] {
@@ -443,11 +432,6 @@ export namespace Doc {
         return bounds;
     }
 
-    export function MakeTitled(title: string) {
-        const doc = new Doc();
-        doc.title = title;
-        return doc;
-    }
     export function MakeAlias(doc: Doc, id?: string) {
         const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
         const layout = Doc.LayoutField(alias);
@@ -488,7 +472,7 @@ export namespace Doc {
         if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
 
             expandedTemplateLayout = undefined;
-        } else if (templateLayoutDoc.resolvedDataDoc === Doc.GetDataDoc(targetDoc)) {
+        } else if (templateLayoutDoc.resolvedDataDoc === Doc.GetProto(targetDoc)) {
             expandedTemplateLayout = templateLayoutDoc;
         } else if (expandedTemplateLayout === undefined) {
             setTimeout(action(() => {
@@ -497,7 +481,7 @@ export namespace Doc {
                     newLayoutDoc.lockedPosition = true;
                     newLayoutDoc.expandedTemplate = targetDoc;
                     targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
-                    const dataDoc = Doc.GetDataDoc(targetDoc);
+                    const dataDoc = Doc.GetProto(targetDoc);
                     newLayoutDoc.resolvedDataDoc = dataDoc;
                     if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && Cast(templateLayoutDoc[templateField], listSpec(Doc), []).length) {
                         dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc: templateLayoutDoc });
@@ -660,10 +644,6 @@ export namespace Doc {
         // assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino)
         Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc);
 
-        if (templateField.backgroundColor !== templateDoc?.defaultBackgroundColor) {
-            templateField.defaultBackgroundColor = templateField.backgroundColor;
-        }
-
         return true;
     }
 
@@ -716,26 +696,26 @@ export namespace Doc {
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
     export function IsBrushed(doc: Doc) {
         return computedFn(function IsBrushed(doc: Doc) {
-            return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc));
+            return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc));
         })(doc);
     }
     // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
     export function IsBrushedDegreeUnmemoized(doc: Doc) {
-        return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
+        return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
     }
     export function IsBrushedDegree(doc: Doc) {
         return computedFn(function IsBrushDegree(doc: Doc) {
-            return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
+            return Doc.IsBrushedDegreeUnmemoized(doc);
         })(doc);
     }
     export function BrushDoc(doc: Doc) {
         brushManager.BrushedDoc.set(doc, true);
-        brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+        brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
         return doc;
     }
     export function UnBrushDoc(doc: Doc) {
         brushManager.BrushedDoc.delete(doc);
-        brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+        brushManager.BrushedDoc.delete(Doc.GetProto(doc));
         return doc;
     }
 
@@ -748,14 +728,14 @@ export namespace Doc {
         document.removeEventListener("pointerdown", linkFollowUnhighlight);
     }
 
-    let dt = 0;
+    let _lastDate = 0;
     export function linkFollowHighlight(destDoc: Doc, dataAndDisplayDocs = true) {
         linkFollowUnhighlight();
         Doc.HighlightDoc(destDoc, dataAndDisplayDocs);
         document.removeEventListener("pointerdown", linkFollowUnhighlight);
         document.addEventListener("pointerdown", linkFollowUnhighlight);
-        const x = dt = Date.now();
-        window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000);
+        const lastDate = _lastDate = Date.now();
+        window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
     }
 
     export class HighlightBrush {
@@ -763,18 +743,18 @@ export namespace Doc {
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-        return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc));
+        return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
     }
     export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
         runInAction(() => {
             highlightManager.HighlightedDoc.set(doc, true);
-            dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), true);
+            dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
         });
     }
     export function UnHighlightDoc(doc: Doc) {
         runInAction(() => {
             highlightManager.HighlightedDoc.set(doc, false);
-            highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), false);
+            highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
         });
     }
     export function UnhighlightAll() {
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 81f073855..3cc05d3d5 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -4,12 +4,13 @@ import { Doc } from "./Doc";
 import { DateField } from "./DateField";
 
 export const documentSchema = createSchema({
-    layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below (see layout_custom as an example)
+    type: "string",             // enumerated type of document -- should be template-specific (ie, start with an '_')
+    layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below
     layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
-    layout_custom: Doc,         // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
     title: "string",            // document title (can be on either data document or layout)
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
     childDropAction: "string",  // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
+    _autoHeight: "boolean",     // whether the height of the document should be computed automatically based on its contents
     _nativeWidth: "number",     // native width of document which determines how much document contents are scaled when the document's width is set
     _nativeHeight: "number",    // "
     _width: "number",           // width of document in its container's coordinate system
@@ -27,20 +28,18 @@ export const documentSchema = createSchema({
     opacity: "number",          // opacity of document
     creationDate: DateField,    // when the document was created
     links: listSpec(Doc),       // computed (readonly) list of links associated with this document
-    removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerUp: ScriptField,   // script to run when document is clicked (can be overriden by an onClick prop)
     onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
+    removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     ignoreAspect: "boolean",    // whether aspect ratio should be ignored when laying out or manipulating the document
-    autoHeight: "boolean",      // whether the height of the document should be computed automatically based on its contents
     isTemplateForField: "string",// when specifies a field key, then the containing document is a template that renders the specified field
     isBackground: "boolean",    // whether document is a background element and ignores input events (can only selet with marquee)
-    type: "string",             // enumerated type of document
     treeViewOpen: "boolean",    //  flag denoting whether the documents sub-tree (contents) is visible or hidden
     treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
-    preventTreeViewOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+    treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
     currentTimecode: "number",  // current play back time of a temporal document (video / audio)
     maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) 
     lockedPosition: "boolean",  // whether the document can be moved (dragged)
@@ -57,7 +56,6 @@ export const documentSchema = createSchema({
     fitToBox: "boolean",        // whether freeform view contents should be zoomed/panned to fill the area of the document view
     xPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
     yPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
-    LODarea: "number",          // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents
     letterSpacing: "string",
     textTransform: "string"
 });
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 45875d455..32c375bd7 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -139,7 +139,7 @@ export class CurrentUserUtils {
     static setupThumbDoc(userDoc: Doc) {
         if (!userDoc.thumbDoc) {
             userDoc.thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
-                _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, isExpanded: true, backgroundColor: "white"
+                _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white"
             });
         }
         return userDoc.thumbDoc;
@@ -181,12 +181,12 @@ export class CurrentUserUtils {
         });
 
         doc.documents = Docs.Create.TreeDocument([], {
-            title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+            title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         // setup Recently Closed library item
         doc.recentlyClosed = Docs.Create.TreeDocument([], {
-            title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+            title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         return Docs.Create.ButtonDocument({
@@ -255,7 +255,7 @@ export class CurrentUserUtils {
             { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: slideTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "sticky-note" });
         doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc, doc.slidesBtn as Doc], {
             title: "expanding buttons", _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0",
-            backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true,
+            backgroundColor: "black", treeViewPreventOpen: true, forceActive: true, lockedPosition: true,
 
             dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
         });
-- 
cgit v1.2.3-70-g09d2


From 2ae91501a3195da9dcbc02e180b108983cd1d8af Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 16 Feb 2020 21:47:28 -0500
Subject: got rid of unneeded ignoreAspect field.   other cleanup.

---
 src/client/util/RichTextSchema.tsx                      |  2 +-
 src/client/views/DocumentDecorations.tsx                |  9 ++-------
 src/client/views/collections/CollectionDockingView.tsx  |  8 ++++----
 .../views/collections/CollectionMasonryViewFieldRow.tsx |  3 ++-
 src/client/views/collections/CollectionStackingView.tsx |  6 +++---
 src/client/views/collections/CollectionView.tsx         | 17 -----------------
 .../views/nodes/CollectionFreeFormDocumentView.tsx      |  2 +-
 src/client/views/nodes/DocumentView.tsx                 | 15 +++++----------
 src/client/views/nodes/ImageBox.tsx                     |  2 +-
 src/client/views/nodes/PDFBox.tsx                       |  4 ++--
 src/new_fields/Doc.ts                                   |  3 +--
 src/new_fields/documentSchemas.ts                       |  1 -
 12 files changed, 22 insertions(+), 50 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 3a3bcd1ad..656215368 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -723,7 +723,7 @@ export class DashDocView {
         const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
         return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
     }
-    contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
+    contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
     outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document);  // ideally, this would scroll to show the focus target
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this._textBox = tbox;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 5fc14e716..cc7388a61 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -283,17 +283,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 const actualdH = Math.max(height + (dH * scale), 20);
                 doc.x = (doc.x || 0) + dX * (actualdW - width);
                 doc.y = (doc.y || 0) + dY * (actualdH - height);
-                const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
-                if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
-                    layoutDoc.ignoreAspect = false;
-                    layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
-                    layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
-                }
+                const fixedAspect = e.ctrlKey || (nwidth && nheight);
                 if (fixedAspect && (!nwidth || !nheight)) {
                     layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
                     layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
                 }
-                if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
+                if (nwidth > 0 && nheight > 0) {
                     if (Math.abs(dW) > Math.abs(dH)) {
                         if (!fixedAspect) {
                             layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6abfb59c3..bf0333d55 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -678,8 +678,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth;
     panelHeight = () => this._panelHeight;
 
-    nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0;
-    nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0;
+    nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0;
+    nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0;
 
     contentScaling = () => {
         if (this.layoutDoc!.type === DocumentType.PDF) {
@@ -705,8 +705,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
         return Transform.Identity();
     }
-    get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
-    get widthpercent() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; }
+    get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
+    get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; }
 
     addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string, libraryPath?: Doc[]) => {
         SelectionManager.DeselectAll();
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index f3dd7c733..c9b7ca221 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -259,7 +259,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
 
     @computed get contentLayout() {
         const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
-        const style = this.props.parent; const collapsed = this._collapsed;
+        const style = this.props.parent;
+        const collapsed = this._collapsed;
         const chromeStatus = this.props.parent.props.Document._chromeStatus;
         const newEditableViewProps = {
             GetValue: () => "",
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index a9cefae6a..ad9dc8f7c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -185,7 +185,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         if (!d) return 0;
         const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
         const nw = NumCast(layoutDoc._nativeWidth);
-        return Math.min(nw && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+        return Math.min(nw && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
     }
     getDocHeight(d?: Doc) {
         if (!d) return 0;
@@ -193,9 +193,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         const nw = NumCast(layoutDoc._nativeWidth);
         const nh = NumCast(layoutDoc._nativeHeight);
         let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
-        if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) {
+        if (!layoutDoc._fitWidth && nw && nh) {
             const aspect = nw && nh ? nh / nw : 1;
-            if (!(!layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
+            if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
             return wid * aspect;
         }
         return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin :
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 48294f9c2..52a18fb99 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -90,11 +90,9 @@ export interface CollectionRenderProps {
 export class CollectionView extends Touchable<FieldViewProps> {
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
 
-    private _reactionDisposer: IReactionDisposer | undefined;
     private _isChildActive = false;   //TODO should this be observable?
     @observable private _isLightboxOpen = false;
     @observable private _curLightboxImg = 0;
-    @observable private _collapsed = true;
     @observable private static _safeMode = false;
     public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
 
@@ -111,20 +109,6 @@ export class CollectionView extends Touchable<FieldViewProps> {
         return viewField;
     }
 
-    componentDidMount = () => {
-        this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus),
-            () => {
-                // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
-                // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
-                const chromeStatus = this.props.Document._chromeStatus;
-                if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed" || chromeStatus === "replaced")) {
-                    runInAction(() => this._collapsed = true);
-                }
-            });
-    }
-
-    componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer();
-
     // bcz: Argh?  What's the height of the collection chromes??  
     chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0);
 
@@ -198,7 +182,6 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     @action
     private collapse = (value: boolean) => {
-        this._collapsed = value;
         this.props.Document._chromeStatus = value ? "collapsed" : "enabled";
     }
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 53d17b580..eaab4086c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -60,7 +60,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return undefined;
     }
 
-    contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect && !this.props.fitToBox ? this.width / this.nativeWidth : 1;
+    contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox ? this.width / this.nativeWidth : 1;
     panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth());
     panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight());
     getTransform = (): Transform => this.props.ScreenToLocalTransform()
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b4ec09aee..330410a92 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -364,18 +364,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 const actualdH = Math.max(height + (dH * scale), 20);
                 doc.x = (doc.x || 0) + dX * (actualdW - width);
                 doc.y = (doc.y || 0) + dY * (actualdH - height);
-                const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
-                if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
-                    layoutDoc.ignoreAspect = false;
-
-                    layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
-                    layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
-                }
+                const fixedAspect = e.ctrlKey || (nwidth && nheight);
                 if (fixedAspect && (!nwidth || !nheight)) {
                     layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
                     layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
                 }
-                if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
+                if (nwidth > 0 && nheight > 0) {
                     if (Math.abs(dW) > Math.abs(dH)) {
                         if (!fixedAspect) {
                             layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
@@ -546,10 +540,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     @undoBatch
-    public static unfreezeNativeDimensions = action((layoutDoc: Doc): void => {
+    @action
+    public static unfreezeNativeDimensions(layoutDoc: Doc) {
         layoutDoc._nativeWidth = undefined;
         layoutDoc._nativeHeight = undefined;
-    });
+    }
 
     toggleNativeDimensions = () => {
         if (this.Document._nativeWidth || this.Document._nativeHeight) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 364bce7a8..5b5768d54 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -375,7 +375,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
         const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
 
-        !this.Document.ignoreAspect && this.resize(srcpath);
+        this.resize(srcpath);
 
         return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
             <div className="imageBox-fader" >
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index e1c5fd27f..a2aec699f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -52,7 +52,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         this._initialScale = this.props.ScreenToLocalTransform().Scale;
         const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927));
         const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200));
-        !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+        !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
 
         const backup = "oldPath";
         const { Document } = this.props;
@@ -93,7 +93,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         this.dataDoc[this.props.fieldKey + "-numPages"] = np;
         this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = nw * 96 / 72;
         this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72;
-        !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+        !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
     }
 
     public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index a722f552e..48890ac8d 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -840,8 +840,7 @@ export namespace Doc {
 
     export function freezeNativeDimensions(layoutDoc: Doc, width: number, height: number): void {
         layoutDoc._autoHeight = false;
-        layoutDoc.ignoreAspect = false;
-        if (!layoutDoc.ignoreAspect && !layoutDoc._nativeWidth) {
+        if (!layoutDoc._nativeWidth) {
             layoutDoc._nativeWidth = NumCast(layoutDoc._width, width);
             layoutDoc._nativeHeight = NumCast(layoutDoc._height, height);
         }
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 3cc05d3d5..f3726cb5a 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -34,7 +34,6 @@ export const documentSchema = createSchema({
     onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
-    ignoreAspect: "boolean",    // whether aspect ratio should be ignored when laying out or manipulating the document
     isTemplateForField: "string",// when specifies a field key, then the containing document is a template that renders the specified field
     isBackground: "boolean",    // whether document is a background element and ignores input events (can only selet with marquee)
     treeViewOpen: "boolean",    //  flag denoting whether the documents sub-tree (contents) is visible or hidden
-- 
cgit v1.2.3-70-g09d2


From 6d7f56cead95b1ab59a62ccc21ee52491386a655 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 18 Feb 2020 14:00:06 -0500
Subject: cleaned up context menu ordering

---
 src/client/views/DocumentDecorations.tsx           |  4 +-
 src/client/views/collections/CollectionView.tsx    | 13 +++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  1 +
 src/client/views/nodes/DocumentView.tsx            | 61 +++++++++++-----------
 src/client/views/nodes/FormattedTextBox.tsx        |  3 +-
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 6 files changed, 46 insertions(+), 38 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8e4598339..07cc1d984 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -171,7 +171,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
     @action onSettingsClick = (e: PointerEvent): void => {
         if (e.button === 0 && !e.altKey && !e.ctrlKey) {
-            this.simulateMouseClick(SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0].children[0], e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
+            let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0];
+            while (child.children.length && child.className !== "jsx-parser") child = child.children[0];
+            this.simulateMouseClick(child.children[0], e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
         }
     }
 
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 52a18fb99..79fd7d792 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -236,15 +236,20 @@ export class CollectionView extends Touchable<FieldViewProps> {
             }
             !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
 
-            const more = ContextMenu.Instance.findByDescription("More...");
-            const moreItems = more && "subitems" in more ? more.subitems : [];
-            moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
-            !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+            const open = ContextMenu.Instance.findByDescription("Open...");
+            const openItems = open && "subitems" in open ? open.subitems : [];
+            !open && ContextMenu.Instance.addItem({ description: "Open...", subitems: openItems, icon: "hand-point-right" });
 
             const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
             const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
             onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
             !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+
+            const more = ContextMenu.Instance.findByDescription("More...");
+            const moreItems = more && "subitems" in more ? more.subitems : [];
+            moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
+            !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+
         }
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 59c3033e9..fa8a203b4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -965,6 +965,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     // }
 
     onContextMenu = (e: React.MouseEvent) => {
+        if (this.props.children && this.props.annotationsKey) return;
         const layoutItems: ContextMenuProps[] = [];
 
         layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5062f193e..229e7fa45 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -628,22 +628,37 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
         e.preventDefault();
 
+        const cm = ContextMenu.Instance;
         const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
 
-        const cm = ContextMenu.Instance;
-        const subitems: ContextMenuProps[] = [];
-        subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" });
-        subitems.push({ description: "Open Tab        ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab", this.props.LibraryPath), icon: "folder" });
-        subitems.push({ description: "Open Right      ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" });
-        subitems.push({ description: "Open Alias Tab  ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
-        subitems.push({ description: "Open Fields     ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
-        templateDoc && subitems.push({ description: "Open Template   ", event: () => this.props.addDocTab(templateDoc, undefined, "onRight"), icon: "eye" });
-        subitems.push({ description: "Open Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
-        cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
-
-
-        const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+        const existing = cm.findByDescription("Layout...");
+        const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+        layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
+        layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+
+        layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+        layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
+        layoutItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" });
+        layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
+        layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+        layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
+        layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
+        !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
+
+        const open = ContextMenu.Instance.findByDescription("Open...");
+        const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
+        openItems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" });
+        openItems.push({ description: "Open Tab        ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab", this.props.LibraryPath), icon: "folder" });
+        openItems.push({ description: "Open Right      ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" });
+        openItems.push({ description: "Open Alias Tab  ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
+        openItems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
+        openItems.push({ description: "Open Fields     ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
+        templateDoc && openItems.push({ description: "Open Template   ", event: () => this.props.addDocTab(templateDoc, undefined, "onRight"), icon: "eye" });
+        openItems.push({ description: "Open Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
+        !open && cm.addItem({ description: "Open...", subitems: openItems, icon: "external-link-alt" });
+
+
+        const existingOnClick = cm.findByDescription("OnClick...");
         const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
         onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
         onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(this, "${this.props.Document.layoutKey}")`), icon: "window-restore" });
@@ -657,24 +672,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
             funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
             funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined });
-            ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
+            cm.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
         }
 
-        const existing = ContextMenu.Instance.findByDescription("Layout...");
-        const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
-        layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
-        layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
-
-        layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
-        layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
-        layoutItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" });
-        layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
-        layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
-        layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
-        layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
-        !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
-
-        const more = ContextMenu.Instance.findByDescription("More...");
+        const more = cm.findByDescription("More...");
         const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : [];
 
         if (!ClientUtils.RELEASE) {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 26711259c..fd288f720 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -990,7 +990,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     richTextMenuPlugin() {
-        const self = FormattedTextBox;
         return new Plugin({
             view(newView) {
                 RichTextMenu.Instance.changeView(newView);
@@ -1091,7 +1090,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         return (
             <div className={`formattedTextBox-cont`} ref={this._ref}
                 style={{
-                    height: this.layoutDoc._autoHeight ? "max-content" : undefined,
+                    height: this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined,
                     background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"]),
                     opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                     color: this.props.hideOnLeave ? "white" : "inherit",
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 5b5768d54..ba09a02a3 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -377,7 +377,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
         this.resize(srcpath);
 
-        return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+        return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget}>
             <div className="imageBox-fader" >
                 <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
                     src={srcpath}
-- 
cgit v1.2.3-70-g09d2


From e816fa75571e2149bbc946dfddb0ca78cf33ca28 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 19 Feb 2020 14:40:34 -0500
Subject: chome display set to 'none' when not 'enabled'. fixed template
 expansion bug.

---
 src/client/views/MainView.tsx                       |  4 ++--
 src/client/views/collections/CollectionView.tsx     |  9 ++++-----
 .../views/collections/CollectionViewChromes.scss    |  3 ++-
 .../views/collections/CollectionViewChromes.tsx     |  7 +++++--
 src/client/views/nodes/ImageBox.tsx                 |  2 +-
 src/new_fields/Doc.ts                               | 21 ++-------------------
 6 files changed, 16 insertions(+), 30 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 1849ec250..cc75a68fe 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -454,11 +454,11 @@ export class MainView extends React.Component {
             {this.docButtons}
         </div>;
     }
-    //`${StrCast(sidebar.backgroundColor, this.darkScheme ? "dimGray" : "black")}` }} >
+
     @computed get mainContent() {
         const sidebar = this.userDoc && this.userDoc.sidebarContainer;
         return !this.userDoc || !(sidebar instanceof Doc) ? (null) : (
-            <div className="mainView-mainContent" style={{ color: this.darkScheme ? "dimGray" : "black" }} >
+            <div className="mainView-mainContent" style={{ color: this.darkScheme ? "rgb(205,205,205)" : "black" }} >
                 <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}>
                     <div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}
                         style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 95f9fa62f..157229ec8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -109,9 +109,6 @@ export class CollectionView extends Touchable<FieldViewProps> {
         return viewField;
     }
 
-    // bcz: Argh?  What's the height of the collection chromes??  
-    chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0);
-
     active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
 
     whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); };
@@ -128,15 +125,17 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     @action.bound
     removeDocument(doc: Doc): boolean {
+        const targetDataDoc = this.props.Document[DataSym];
         const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
         docView && SelectionManager.DeselectDoc(docView);
-        const value = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []);
+        const value = DocListCast(targetDataDoc[this.props.fieldKey]);
         let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
         index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
 
         ContextMenu.Instance.clearItems();
         if (index !== -1) {
             value.splice(index, 1);
+            targetDataDoc[this.props.fieldKey] = new List<Doc>(value);
             return true;
         }
         return false;
@@ -162,7 +161,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     }
 
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
-        const props: SubCollectionViewProps = { ...this.props, ...renderProps, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
+        const props: SubCollectionViewProps = { ...this.props, ...renderProps, CollectionView: this, annotationsKey: "" };
         switch (type) {
             case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />);
             case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 517f467b7..0c96a662c 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -2,7 +2,8 @@
 @import '~js-datepicker/dist/datepicker.min.css';
 
 .collectionViewChrome-cont {
-    position: relative;
+    position: absolute;
+    width:100%;
     opacity: 0.9;
     z-index: 9001;
     transition: top .5s;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 0378c818c..49a9e8200 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -257,6 +257,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     }
 
     subChrome = () => {
+        const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+        if (collapsed) return null;
         switch (this.props.type) {
             case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
             case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
@@ -368,7 +370,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
         const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
         return (
             <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
-                <div className="collectionViewChrome">
+                <div className="collectionViewChrome" style={{ border: "unset" }}>
                     <div className="collectionViewBaseChrome">
                         <button className="collectionViewBaseChrome-collapse"
                             style={{
@@ -384,6 +386,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                             className="collectionViewBaseChrome-viewPicker"
                             onPointerDown={stopPropagation}
                             onChange={this.viewChanged}
+                            style={{ display: collapsed ? "none" : undefined }}
                             value={NumCast(this.props.CollectionView.props.Document._viewType)}>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema</option>
@@ -438,7 +441,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                                 </div>
                             </div>
                         </div>
-                        <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} >
+                        <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
                             <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
                                 <div className="commandEntry-drop">
                                     <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ba09a02a3..bb421d315 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -96,7 +96,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 if (targetDoc[targetField] instanceof ImageField) {
                     this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
                     this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]);
-                    this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(targeDoc[targetField + "-nativeHeight"]);
+                    this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(targetDoc[targetField + "-nativeHeight"]);
                     e.stopPropagation();
                 }
             }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1f2dd1c8f..2c7fc6672 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -471,7 +471,6 @@ export namespace Doc {
         const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + "]";
         let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
         if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
-
             expandedTemplateLayout = undefined;
         } else if (templateLayoutDoc.resolvedDataDoc === Doc.GetProto(targetDoc)) {
             expandedTemplateLayout = templateLayoutDoc;
@@ -484,7 +483,7 @@ export namespace Doc {
                     targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
                     const dataDoc = Doc.GetProto(targetDoc);
                     newLayoutDoc.resolvedDataDoc = dataDoc;
-                    if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && Cast(templateLayoutDoc[templateField], listSpec(Doc), []).length) {
+                    if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List) {
                         dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc: templateLayoutDoc });
                     }
                 }
@@ -497,25 +496,9 @@ export namespace Doc {
     // otherwise, it just returns the childDoc
     export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
         const existingResolvedDataDoc = childDoc[DataSym] !== Doc.GetProto(childDoc)[DataSym] && childDoc[DataSym];
-        const resolvedDataDoc = existingResolvedDataDoc || (containerDataDoc === containerDoc || !containerDataDoc || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc);
+        const resolvedDataDoc = existingResolvedDataDoc || (Doc.AreProtosEqual(containerDataDoc, containerDoc) || !containerDataDoc || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc);
         return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
     }
-    export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-        let proto: Doc | undefined = doc;
-        while (proto && !Doc.IsPrototype(proto) && proto.proto) {
-            proto = proto.proto;
-        }
-        let docExtensionForField = ((proto || doc)[fieldKey + "_ext"] as Doc);
-        if (!docExtensionForField) {
-            docExtensionForField = new Doc(doc[Id] + fieldKey, true);
-            docExtensionForField.title = fieldKey + ".ext"; // courtesy field--- shouldn't be needed except maybe for debugging
-            docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
-            docExtensionForField.extendsField = fieldKey; // this can be used by search to map matches on the extension doc back to the field that was extended.
-            docExtensionForField.type = DocumentType.EXTENSION;
-            (proto || doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
-        }
-        return docExtensionForField;
-    }
 
     export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
         Object.keys(doc).forEach(key => {
-- 
cgit v1.2.3-70-g09d2


From 59e947ad4bd885c4bbf511a19eba64f672a3d76d Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 22 Feb 2020 12:14:27 -0500
Subject: added option to update image native size

---
 src/client/views/collections/CollectionSubView.tsx |  4 ++--
 src/client/views/nodes/ImageBox.tsx                | 13 +++++++++++++
 2 files changed, 15 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 27a1c6367..6ad187e5c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -334,8 +334,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 const proto = Doc.GetProto(doc);
                 proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
                 if (Upload.isImageInformation(result)) {
-                    proto["data-nativeWidth"] = result.nativeWidth;
-                    proto["data-nativeHeight"] = result.nativeHeight;
+                    proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400;
+                    proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight);
                     proto.contentSize = result.contentSize;
                 }
                 generatedDocuments.push(doc);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index bb421d315..99000a0a9 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -160,6 +160,19 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             const funcs: ContextMenuProps[] = [];
             funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
             funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
+            funcs.push({
+                description: "Reset Native Dimensions", event: action(() => {
+                    const curNW = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]);
+                    const curNH = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"]);
+                    if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
+                        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
+                        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
+                    } else {
+                        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
+                        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
+                    }
+                }), icon: "expand-arrows-alt"
+            });
 
             const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
             const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
-- 
cgit v1.2.3-70-g09d2


From 5c139f0001cd10e5699a716c1e64d2c2c0b2d800 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 28 Feb 2020 12:17:03 -0500
Subject: allowing different text templates to be the default.  fixing
 transparency.  removing audio by default from notes.

---
 src/client/documents/Documents.ts                  |  5 +++-
 src/client/util/Import & Export/ImageUtils.ts      |  2 +-
 src/client/util/RichTextSchema.tsx                 |  8 ++---
 src/client/views/InkingControl.tsx                 |  3 +-
 src/client/views/TemplateMenu.tsx                  |  5 ++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +--
 .../collections/collectionFreeForm/MarqueeView.tsx |  3 +-
 src/client/views/nodes/DocumentView.tsx            |  6 ++--
 src/client/views/nodes/FormattedTextBox.scss       |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 34 +++++++++++++---------
 src/client/views/nodes/ImageBox.tsx                | 19 ++++++------
 src/new_fields/documentSchemas.ts                  | 13 +++++----
 12 files changed, 63 insertions(+), 41 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index a0b8a6382..aab44f668 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -77,6 +77,8 @@ export interface DocumentOptions {
     _gridGap?: number; // gap between items in masonry view
     _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
     _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
+    _xPadding?: number;
+    _yPadding?: number;
     _itemIndex?: number; // which item index the carousel viewer is showing
     _showSidebar?: boolean;  //whether an annotationsidebar should be displayed for text docuemnts
     x?: number;
@@ -174,7 +176,7 @@ export namespace Docs {
         const TemplateMap: TemplateMap = new Map([
             [DocumentType.TEXT, {
                 layout: { view: FormattedTextBox, dataField: data },
-                options: { _height: 150 }
+                options: { _height: 150, _xMargin: 10, _yMargin: 10 }
             }],
             [DocumentType.HIST, {
                 layout: { view: HistogramBox, dataField: data },
@@ -407,6 +409,7 @@ export namespace Docs {
 
         const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "childDropAction", "_annotationOn",
             "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor",
+            "_xMargin", "_yMargin", "_xPadding", "_yPadding",
             "_color", "isButton", "isBackground", "removeDropProperties", "treeViewOpen"];
 
         /**
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index ff909cc6b..ab8c73d15 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -24,7 +24,7 @@ export namespace ImageUtils {
         const proto = Doc.GetProto(document);
         proto["data-nativeWidth"] = nativeWidth;
         proto["data-nativeHeight"] = nativeHeight;
-        proto.contentSize = contentSize;
+        proto.contentSize = contentSize ? contentSize : undefined;
         return data !== undefined;
     };
 
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a56aac9b1..4a80a1af8 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -910,11 +910,11 @@ export class DashFieldView {
                 }
                 e.preventDefault();
             }
-            if (e.key === "Enter" && e.ctrlKey) {
-                Doc.addEnumerationToTextField(self._textBoxDoc, node.attrs.fieldKey, [Docs.Create.TextDocument(self._fieldSpan.innerText, { title: self._fieldSpan.innerText })]);
-                e.preventDefault();
-            } else if (e.key === "Enter") {
+            if (e.key === "Enter") {
                 e.preventDefault();
+                if (e.ctrlKey) {
+                    Doc.addEnumerationToTextField(self._textBoxDoc, node.attrs.fieldKey, [Docs.Create.TextDocument(self._fieldSpan.innerText, { title: self._fieldSpan.innerText })]);
+                }
                 let newText = self._fieldSpan.innerText.startsWith(":=") ? ":=-computed-" : self._fieldSpan.innerText;
                 // look for a document whose id === the fieldKey being displayed.  If there's a match, then that document
                 // holds the different enumerated values for the field in the titles of its collected documents.
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 374c2df27..5cd3c265d 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -29,8 +29,7 @@ export class InkingControl {
         if (number < 0) {
             number = 0xFFFFFFFF + number + 1;
         }
-
-        return number.toString(16).toUpperCase();
+        return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
     }
 
     @undoBatch
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 595c3817e..5029b4074 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -61,6 +61,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         DocumentView.FloatDoc(topDocView, ex, ey);
     }
 
+    toggleAudio = (e: React.ChangeEvent<HTMLInputElement>): void => {
+        this.props.docViews.map(dv => dv.props.Document._showAudio = e.target.checked);
+    }
+
     @undoBatch
     @action
     toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
@@ -102,6 +106,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         const templateMenu: Array<JSX.Element> = [];
         this.props.templates.forEach((checked, template) =>
             templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
+        templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={this.props.docViews[0].Document._showAudio ? true : false} toggle={this.toggleAudio} />);
         templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docViews[0].Document.z ? true : false} toggle={this.toggleFloat} />);
         templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
         this._addedKeys && Array.from(this._addedKeys).map(layout =>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4458c7dcf..ca8d5e18b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -51,8 +51,8 @@ export const panZoomSchema = createSchema({
     arrangeInit: ScriptField,
     useClusters: "boolean",
     fitToBox: "boolean",
-    xPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
-    yPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+    _xPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+    _yPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
     panTransformType: "string",
     scrollHeight: "number",
     fitX: "number",
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b1cca027d..b8e9acf85 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -18,6 +18,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import "./MarqueeView.scss";
 import React = require("react");
 import { CollectionView } from "../CollectionView";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -104,7 +105,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             });
         } else if (!e.ctrlKey) {
             this.props.addLiveTextDocument(
-                Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }));
+                Docs.Create.TextDocument("", { _width: NumCast((FormattedTextBox.DefaultLayout as Doc)?._width) || 200, _height: 100, layout: FormattedTextBox.DefaultLayout, x: x, y: y, _autoHeight: true, title: "-typed text-" }));
         } else if (e.keyCode > 48 && e.keyCode <= 57) {
             const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
             const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 06ca9b5f4..7683ad269 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -909,17 +909,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     render() {
         if (!(this.props.Document instanceof Doc)) return (null);
         const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
+        const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor;
         const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
         const borderRounding = this.layoutDoc.borderRounding;
         const localScale = fullDegree;
 
+        console.log("Background = " + this.props.Document.title + " " + finalColor);
         const highlightColors = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ?
             ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] :
             ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
         const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
         let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
         highlighting = highlighting && this.props.focus !== emptyFunction;  // bcz: hack to turn off highlighting onsidebar panel documents.  need to flag a document as not highlightable in a more direct way
-        return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
+        return <div id={this.props.Document[Id]} className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
             onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
             onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
             style={{
@@ -931,7 +933,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
                 border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
                 boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined,
-                background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor,
+                background: finalColor,
                 width: "100%",
                 height: "100%",
                 opacity: this.Document.opacity
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index c203ca0c3..db2bb751f 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -95,8 +95,8 @@
 
 .formattedTextBox-inner-rounded,
 .formattedTextBox-inner {
-    padding: 10px 10px;
     height: 100%;
+    white-space: pre-wrap; 
 }
 
 // .menuicon {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index a320cff75..3567dbbd5 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -375,12 +375,15 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%");
 
+    public static DefaultLayout: Doc | string | undefined;
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
+        this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => FormattedTextBox.DefaultLayout = this.props.Document.proto as Doc, icon: "eye" });
+        funcs.push({ description: "Reset Default Layout", event: () => FormattedTextBox.DefaultLayout = undefined, icon: "eye" });
         !this.props.Document.expandedTemplate && funcs.push({ description: "Make Template", event: () => { this.props.Document.isTemplateDoc = true; Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document); }, icon: "eye" });
-        funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.props.Document._showSidebar = !this.props.Document._showSidebar; }, icon: "expand-arrows-alt" });
-        funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
-        funcs.push({ description: "Toggle Menubar", event: () => { e.stopPropagation(); this.toggleMenubar(); }, icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
+        funcs.push({ description: "Record Bullet", event: () => this.recordBullet(), icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
         ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
                 description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
@@ -1125,7 +1128,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 onPointerLeave={action(() => this._entered = false)}
             >
                 <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}>
-                    <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
+                    <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget}
+                        style={{
+                            padding: `${NumCast(this.Document._xMargin, 0)}px  ${NumCast(this.Document._yMargin, 0)}px`,
+                            pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined
+                        }} />
                 </div>
                 {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                     <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
@@ -1152,15 +1159,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                         </CollectionFreeFormView>
                         <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} />
                     </div>}
-                <div className="formattedTextBox-dictation"
-                    onClick={e => {
-                        this._recording ? this.stopDictation(true) : this.recordDictation();
-                        setTimeout(() => this._editorView!.focus(), 500);
-                        e.stopPropagation();
-                    }} >
-                    <FontAwesomeIcon className="formattedTExtBox-audioFont"
-                        style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
-                </div>
+                {!this.props.Document._showAudio ? (null) :
+                    <div className="formattedTextBox-dictation"
+                        onClick={e => {
+                            this._recording ? this.stopDictation(true) : this.recordDictation();
+                            setTimeout(() => this._editorView!.focus(), 500);
+                            e.stopPropagation();
+                        }} >
+                        <FontAwesomeIcon className="formattedTExtBox-audioFont"
+                            style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
+                    </div>}
             </div>
         );
     }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 99000a0a9..c46191270 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -407,15 +407,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                         ref={this._imgRef}
                         onError={this.onError} /></div>}
             </div>
-            <div className="imageBox-audioBackground"
-                onPointerDown={this.audioDown}
-                onPointerEnter={this.onPointerEnter}
-                style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
-            >
-                <FontAwesomeIcon className="imageBox-audioFont"
-                    style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
-                    icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
-            </div>
+            {!this.props.Document._showAudio ? (null) :
+                <div className="imageBox-audioBackground"
+                    onPointerDown={this.audioDown}
+                    onPointerEnter={this.onPointerEnter}
+                    style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
+                >
+                    <FontAwesomeIcon className="imageBox-audioFont"
+                        style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
+                        icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
+                </div>}
             {this.considerDownloadIcon}
             {this.considerGooglePhotosLink()}
             <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index f3726cb5a..7006163e0 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -15,9 +15,14 @@ export const documentSchema = createSchema({
     _nativeHeight: "number",    // "
     _width: "number",           // width of document in its container's coordinate system
     _height: "number",          // "
-    _showCaption: "string",      // whether editable caption text is overlayed at the bottom of the document 
-    _showTitle: "string",        // the fieldkey whose contents should be displayed at the top of the document
-    _showTitleHover: "string",   // the showTitle should be shown only on hover
+    _xPadding: "number",        // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+    _yPadding: "number",        // pixels of padding on top/bottom of collectionfreeformview contents when fitToBox is set
+    _xMargin: "number",         // margin added on left/right of most documents to add separation from their container
+    _yMargin: "number",         // margin added on top/bottom of most documents to add separation from their container
+    _showCaption: "string",     // whether editable caption text is overlayed at the bottom of the document 
+    _showTitle: "string",       // the fieldkey whose contents should be displayed at the top of the document
+    _showTitleHover: "string",  // the showTitle should be shown only on hover
+    _showAudio: "boolean",      // whether to show the audio record icon on documents
     _freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents
     _LODdisable: "boolean",     // whether to disbale LOD switching for CollectionFreeFormViews
     _pivotField: "string",      // specifies which field should be used as the timeline/pivot axis
@@ -53,8 +58,6 @@ export const documentSchema = createSchema({
     strokeWidth: "number",
     fontSize: "string",
     fitToBox: "boolean",        // whether freeform view contents should be zoomed/panned to fill the area of the document view
-    xPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
-    yPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
     letterSpacing: "string",
     textTransform: "string"
 });
-- 
cgit v1.2.3-70-g09d2


From 4eaec2585a4f38a826707f2cf850d287276d9b14 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 10 Mar 2020 00:39:30 -0400
Subject: fixed up iconifying of different document types.  fixed selection to
 not select toolbar tabs.   fixed outline-like document creation in tree
 views.

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/views/DocumentDecorations.tsx           |  1 +
 .../views/collections/CollectionTreeView.tsx       |  6 +++--
 src/client/views/nodes/DocumentView.tsx            | 28 ++++++++++++++++------
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/new_fields/Doc.ts                              |  2 +-
 .../authentication/models/current_user_utils.ts    | 16 +++++++++----
 7 files changed, 40 insertions(+), 16 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 901b3684f..131a48a2b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -114,6 +114,7 @@ export interface DocumentOptions {
     lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
     opacity?: number;
     defaultBackgroundColor?: string;
+    dontSelect?: boolean; // whether document decorations should be displayed when the document is selected
     isBackground?: boolean;
     isButton?: boolean;
     columnWidth?: number;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2ec170ddb..c4abc935f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -69,6 +69,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     get Bounds(): { x: number, y: number, b: number, r: number } {
         return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
             if (documentView.props.renderDepth === 0 ||
+                documentView.props.Document.dontSelect ||
                 Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
                 return bounds;
             }
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 54ad2ad48..4757e5a53 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -185,8 +185,10 @@ class TreeView extends React.Component<TreeViewProps> {
         })}
         OnFillDown={undoBatch((value: string) => {
             Doc.SetInPlace(this.props.document, key, value, false);
-            const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
-            EditableView.loadId = doc[Id];
+            const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
+            //EditableView.loadId = doc[Id];
+            this.props.document.editTitle = undefined;
+            doc.editTitle = true;
             return this.props.addDocument(doc);
         })}
         onClick={() => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dc529b79b..bf5f936d1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -281,6 +281,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
             e.stopPropagation();
             let preventDefault = true;
+            this.props.bringToFront(this.props.Document);
             if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
                 const fullScreenAlias = Doc.MakeAlias(this.props.Document);
                 if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
@@ -291,7 +292,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 Doc.UnBrushDoc(this.props.Document);
             } else if (this.onClickHandler?.script) {
                 SelectionManager.DeselectAll();
-                UndoManager.RunInBatch(() => this.onClickHandler!.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log) && this.select(false), "on click");
+                UndoManager.RunInBatch(() => this.onClickHandler!.script.run({
+                    this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, // try this.props.Document.expandedTemplate || this.props.Document
+                    containingCollection: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey
+                }, console.log) && !BoolCast(this.props.Document.dontSelect) && this.select(false), "on click");
             } else if (this.Document.type === DocumentType.BUTTON) {
                 UndoManager.RunInBatch(() => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
             } else if (this.Document.isButton) {
@@ -633,13 +637,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             if (custom) {
                 DocumentView.makeNativeViewClicked(this.props.Document);
 
+                const imgView = Cast(Doc.UserDoc().iconView, Doc, null);
+                const iconImgView = Cast(Doc.UserDoc().iconImageView, Doc, null);
+                const iconColView = Cast(Doc.UserDoc().iconColView, Doc, null);
+                const iconViews = [imgView, iconImgView, iconColView];
+                const expandingButtons = DocListCast(Cast(Doc.UserDoc().expandingButtons, Doc, null)?.data);
+                const allTemplates = iconViews.concat(expandingButtons);
                 let foundLayout: Opt<Doc>;
-                DocListCast(Cast(Doc.UserDoc().expandingButtons, Doc, null)?.data)?.concat([Cast(Doc.UserDoc().iconView, Doc, null)]).
-                    map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc).forEach(tempDoc => {
-                        if (StrCast(tempDoc.title) === layout) {
-                            foundLayout = tempDoc;
-                        }
-                    });
+                allTemplates.map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc).forEach(tempDoc => {
+                    if (StrCast(tempDoc.title) === this.props.Document.type + "_" + layout) {
+                        foundLayout = tempDoc;
+                    }
+                });
+                !foundLayout && allTemplates.map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc).forEach(tempDoc => {
+                    if (StrCast(tempDoc.title) === layout) {
+                        foundLayout = tempDoc;
+                    }
+                });
                 DocumentView.
                     makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout);
             } else {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index e5848614c..6ae32a1d6 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -258,7 +258,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"])
         };
         const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"];
-        if (!cachedNativeSize.width || !cachedNativeSize.height || imgPath !== cachedImgPath) {
+        if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05 || imgPath !== cachedImgPath) {
             (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) && requestImageSize(imgPath).then((inquiredSize: any) => {
                 const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
                 const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 4a6ff8e42..c7caa853a 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -792,7 +792,7 @@ export namespace Doc {
     export function setNativeView(doc: any) {
         const prevLayout = StrCast(doc.layoutKey).split("_")[1];
         const deiconify = prevLayout === "icon" && StrCast(doc.deiconifyLayout) ? "layout_" + StrCast(doc.deiconifyLayout) : "";
-        doc.deiconifyLayout = undefined;
+        prevLayout === "icon" && (doc.deiconifyLayout = undefined);
         if (StrCast(doc.title).endsWith("_" + prevLayout) && deiconify) doc.title = StrCast(doc.title).replace("_" + prevLayout, deiconify);
         else doc.title = undefined;
         doc.layoutKey = deiconify || "layout";
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 5d93f208a..15f626af4 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -11,7 +11,7 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
 import { Cast, PromiseValue, StrCast } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
-import { nullAudio } from "../../../new_fields/URLField";
+import { nullAudio, ImageField } from "../../../new_fields/URLField";
 import { DragManager } from "../../../client/util/DragManager";
 import { InkingControl } from "../../../client/views/InkingControl";
 import { Scripting } from "../../../client/util/Scripting";
@@ -194,7 +194,7 @@ export class CurrentUserUtils {
         });
 
         return Docs.Create.ButtonDocument({
-            _width: 35, _height: 25, title: "Tools", fontSize: 10, targetContainer: sidebarContainer,
+            _width: 35, _height: 25, title: "Tools", fontSize: 10, targetContainer: sidebarContainer, dontSelect: true,
             letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
             sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
                 _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack"
@@ -220,7 +220,7 @@ export class CurrentUserUtils {
         });
 
         return Docs.Create.ButtonDocument({
-            _width: 50, _height: 25, title: "Library", fontSize: 10,
+            _width: 50, _height: 25, title: "Library", fontSize: 10, dontSelect: true,
             letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
             sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, Docs.Prototypes.MainLinkDocument(), doc, doc.recentlyClosed as Doc], {
                 title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true
@@ -233,7 +233,7 @@ export class CurrentUserUtils {
     // setup the Search button which will display the search panel.  
     static setupSearchPanel(sidebarContainer: Doc) {
         return Docs.Create.ButtonDocument({
-            _width: 50, _height: 25, title: "Search", fontSize: 10,
+            _width: 50, _height: 25, title: "Search", fontSize: 10, dontSelect: true,
             letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
             sourcePanel: Docs.Create.QueryDocument({
                 title: "search stack", ignoreClick: true
@@ -275,9 +275,15 @@ export class CurrentUserUtils {
         descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView");
 
         const iconDoc = Docs.Create.TextDocument("", { title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onClick: ScriptField.MakeScript("setNativeView(this)") });
-        Doc.GetProto(iconDoc).data = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
+        Doc.GetProto(iconDoc).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
         doc.isTemplateDoc = makeTemplate(iconDoc);
         doc.iconView = new PrefetchProxy(iconDoc);
+        const imgIconDoc = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { title: "data", _width: 50, isTemplateDoc: true, onClick: ScriptField.MakeScript("setNativeView(this)") });
+        doc.isTemplateDoc = makeTemplate(imgIconDoc, true, "image_icon");
+        doc.iconImageView = new PrefetchProxy(imgIconDoc);
+        const colIconDoc = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, isTemplateDoc: true, onClick: ScriptField.MakeScript("setNativeView(this)") });
+        doc.isTemplateDoc = makeTemplate(colIconDoc, true, "collection_icon");
+        doc.iconColView = new PrefetchProxy(colIconDoc);
 
         doc.undoBtn = Docs.Create.FontIconDocument(
             { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
-- 
cgit v1.2.3-70-g09d2


From 1693e9daf1657c1fc45afa3fa14f0a0df0e01e13 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 17 Mar 2020 16:12:49 -0400
Subject: fixes for typing url in web box.  fixes for dropping imags from
 proxied document.  possible fix for updateing image width/height

---
 src/client/views/collections/CollectionSubView.tsx | 12 +++++---
 src/client/views/nodes/ImageBox.tsx                | 35 ++++++++++++----------
 src/client/views/nodes/WebBox.tsx                  |  6 ++--
 src/server/server_Initialization.ts                |  2 +-
 4 files changed, 31 insertions(+), 24 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b995fc7d5..2d6777a4e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -234,7 +234,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (!html.startsWith("<a")) {
                     const tags = html.split("<");
                     if (tags[0] === "") tags.splice(0, 1);
-                    const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
+                    let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
+                    const cors = img.includes("corsProxy") ? img.match(/http.*corsProxy\//)![0] : "";
+                    img = cors ? img.replace(cors, "") : img;
                     if (img) {
                         const split = img.split("src=\"")[1].split("\"")[0];
                         let source = split;
@@ -242,9 +244,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                             const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [split] });
                             source = Utils.prepend(accessPaths.agnostic.client);
                         }
-                        const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
-                        ImageUtils.ExtractExif(doc);
-                        addDocument(doc);
+                        if (source.startsWith("http")) {
+                            const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
+                            ImageUtils.ExtractExif(doc);
+                            addDocument(doc);
+                        }
                         return;
                     } else {
                         const path = window.location.origin + "/doc/";
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6ae32a1d6..f870a5df4 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -258,22 +258,27 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"])
         };
         const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"];
+        const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
+        const cachedAspect = cachedNativeSize.height / cachedNativeSize.width;
         if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05 || imgPath !== cachedImgPath) {
-            (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) && requestImageSize(imgPath).then((inquiredSize: any) => {
-                const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
-                const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
-                const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width;
-                const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
-                setTimeout(action(() => {
-                    if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) {
-                        this.Document._height = this.Document[WidthSym]() * rotatedAspect;
-                        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
-                        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
-                    }
-                    this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath;
-                }), 0);
-            })
-                .catch((err: any) => console.log(err));
+            if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) {
+                requestImageSize(imgPath).then((inquiredSize: any) => {
+                    const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
+                    const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
+                    const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width;
+                    setTimeout(action(() => {
+                        if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) {
+                            this.Document._height = this.Document[WidthSym]() * rotatedAspect;
+                            this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
+                            this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
+                        }
+                        this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath;
+                    }), 0);
+                }).catch((err: any) => console.log(err));
+            } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) {
+                this.Document._width = this.Document[WidthSym]() || cachedNativeSize.width;
+                this.Document._height = this.Document[WidthSym]() * cachedAspect;
+            }
         } else if (this.Document._nativeWidth !== cachedNativeSize.width || this.Document._nativeHeight !== cachedNativeSize.height) {
             !(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => {
                 if (!(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc)) {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 24cb08c4c..557adfc03 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -81,14 +81,12 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
 
     @action
     submitURL = () => {
-        const script = KeyValueBox.CompileKVPScript(`new WebField("${this.url}")`);
-        if (!script) return;
-        KeyValueBox.ApplyKVPScript(this.props.Document, "data", script);
+        this.dataDoc[this.props.fieldKey] = new WebField(new URL(this.url));
     }
 
     @action
     setURL() {
-        const urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField);
+        const urlField: FieldResult<WebField> = Cast(this.dataDoc[this.props.fieldKey], WebField);
         if (urlField) this.url = urlField.url.toString();
         else this.url = "";
     }
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 76cb22521..7b2228831 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -129,7 +129,7 @@ function registerCorsProxy(server: express.Express) {
         // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative, 
         // then we redirect again to the cors referer and just add the relative path.
         if (!requrl.startsWith("http") && req.originalUrl.startsWith("/corsProxy") && referer?.includes("corsProxy")) {
-            res.redirect(referer + requrl);
+            res.redirect(referer + (referer.endsWith("/") ? "" : "/") + requrl);
         }
         else {
             req.pipe(request(requrl)).on("response", res => {
-- 
cgit v1.2.3-70-g09d2


From 85a67ff30d86fa51b4534f717d3857aa23b732d1 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 19 Mar 2020 12:04:32 -0400
Subject: added screen capture doc

---
 src/client/documents/DocumentTypes.ts              |   3 +-
 src/client/documents/Documents.ts                  |   9 +
 src/client/views/collections/CollectionSubView.tsx |   2 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   3 +-
 src/client/views/nodes/ImageBox.tsx                |  27 ++-
 src/client/views/nodes/ScreenshotBox.scss          |  46 +++++
 src/client/views/nodes/ScreenshotBox.tsx           | 193 +++++++++++++++++++++
 src/client/views/nodes/VideoBox.tsx                |  17 +-
 src/client/views/nodes/WebBox.scss                 |   7 +-
 src/client/views/nodes/WebBox.tsx                  |   9 +-
 .../authentication/models/current_user_utils.ts    |   3 +-
 11 files changed, 296 insertions(+), 23 deletions(-)
 create mode 100644 src/client/views/nodes/ScreenshotBox.scss
 create mode 100644 src/client/views/nodes/ScreenshotBox.tsx

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index e3ce8e798..5ec1cfdb4 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -27,5 +27,6 @@ export enum DocumentType {
     DOCULINK = "doculink",
     PDFANNO = "pdfanno",
     INK = "ink",
-    DOCUMENT = "document"
+    DOCUMENT = "document",
+    SCREENSHOT = "screenshot",
 }
\ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 131a48a2b..b5cffaddd 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -58,6 +58,7 @@ import { MessageStore } from "../../server/Message";
 import { ContextMenuProps } from "../views/ContextMenuItem";
 import { ContextMenu } from "../views/ContextMenu";
 import { LinkBox } from "../views/nodes/LinkBox";
+import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
 const requestImageSize = require('../util/request-image-size');
 const path = require('path');
 
@@ -277,6 +278,10 @@ export namespace Docs {
             [DocumentType.INK, {
                 layout: { view: InkingStroke, dataField: data },
                 options: { backgroundColor: "transparent" }
+            }],
+            [DocumentType.SCREENSHOT, {
+                layout: { view: ScreenshotBox, dataField: data },
+                options: {}
             }]
         ]);
 
@@ -522,6 +527,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options);
         }
 
+        export function ScreenshotDocument(url: string, options: DocumentOptions = {}) {
+            return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options);
+        }
+
         export function AudioDocument(url: string, options: DocumentOptions = {}) {
             const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
             Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'");
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 2d6777a4e..97177855f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -316,7 +316,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 const item = e.dataTransfer.items[i];
                 if (item.kind === "string" && item.type.includes("uri")) {
                     const stringContents = await new Promise<string>(resolve => item.getAsString(resolve));
-                    const type = (await rp.head(Utils.CorsProxy(stringContents)))["content-type"];
+                    const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"];
                     if (type) {
                         const doc = await Docs.Get.DocumentFromType(type, stringContents, options);
                         doc && generatedDocuments.push(doc);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index f121ba3e0..239f414fd 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -29,6 +29,7 @@ import { ColorBox } from "./ColorBox";
 import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
 import { DocuLinkBox } from "./DocuLinkBox";
 import { PresElementBox } from "../presentationview/PresElementBox";
+import { ScreenshotBox } from "./ScreenshotBox";
 import { VideoBox } from "./VideoBox";
 import { WebBox } from "./WebBox";
 import { InkingStroke } from "../InkingStroke";
@@ -99,7 +100,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
                     CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
                     PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
                     ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox,
-                    RecommendationsBox,
+                    RecommendationsBox, ScreenshotBox
                 }}
                 bindings={this.CreateBindings()}
                 jsx={this.layout}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f870a5df4..b1c172e22 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,37 +1,34 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEye } from '@fortawesome/free-regular-svg-icons';
-import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons';
+import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction, trace } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
+import { ObjectField } from '../../../new_fields/ObjectField';
 import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
 import { ComputedField } from '../../../new_fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
 import { AudioField, ImageField } from '../../../new_fields/URLField';
-import { Utils, returnOne, emptyFunction } from '../../../Utils';
+import { TraceMobx } from '../../../new_fields/util';
+import { emptyFunction, returnOne, Utils } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs } from '../../documents/Documents';
+import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from "../../views/ContextMenu";
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocAnnotatableComponent } from '../DocComponent';
 import FaceRectangles from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ImageBox.scss";
 import React = require("react");
-import { SearchUtil } from '../../util/SearchUtil';
-import { ClientRecommender } from '../../ClientRecommender';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id, Copy } from '../../../new_fields/FieldSymbols';
-import { TraceMobx } from '../../../new_fields/util';
-import { SelectionManager } from '../../util/SelectionManager';
-import { cache } from 'sharp';
-import { ObjectField } from '../../../new_fields/ObjectField';
-import { Networking } from '../../Network';
 const requestImageSize = require('../../util/request-image-size');
 const path = require('path');
 const { Howl } = require('howler');
@@ -163,7 +160,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
             funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
             funcs.push({
-                description: "Reset Native Dimensions", event: action(() => {
+                description: "Reset Native Dimensions", event: action(async () => {
                     const curNW = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]);
                     const curNH = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"]);
                     if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss
new file mode 100644
index 000000000..4aaccb472
--- /dev/null
+++ b/src/client/views/nodes/ScreenshotBox.scss
@@ -0,0 +1,46 @@
+.screenshotBox {
+    pointer-events: all;
+    transform-origin: top left;
+    background: white;
+    color: black;
+    // .screenshotBox-viewer {
+    //     opacity: 0.99;  // hack!  overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
+    // }
+    // .inkingCanvas-paths-markers {
+    //     opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
+    // }
+}
+
+.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-cont-fullScreen {
+    width: 100%;
+    z-index: -1; // 0;  // logically this should be 0 (or unset) which would give us transparent brush strokes over videos.  However, this makes Chrome crawl to a halt
+    position: absolute;
+}
+
+.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-content-fullScreen {
+    height: Auto; 
+}
+
+.screenshotBox-content-interactive, .screenshotBox-content-fullScreen  {
+    pointer-events: all;
+}
+
+.screenshotBox-snapshot{
+    color : white;
+    top :25px;
+    right : 25px;
+    position: absolute;
+    background-color:rgba(50, 50, 50, 0.2);
+    transform-origin: left top;
+    pointer-events:all;
+}
+
+.screenshotBox-recorder{
+    color : white;
+    top :25px;
+    right : 50px;
+    position: absolute;
+    background-color:rgba(50, 50, 50, 0.2);
+    transform-origin: left top;
+    pointer-events:all;
+}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
new file mode 100644
index 000000000..de9e521f6
--- /dev/null
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -0,0 +1,193 @@
+import React = require("react");
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faVideo } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as rp from 'request-promise';
+import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
+import { makeInterface } from "../../../new_fields/Schema";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
+import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./ScreenshotBox.scss";
+const path = require('path');
+
+type ScreenshotDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+const ScreenshotDocument = makeInterface(documentSchema, positionSchema);
+
+library.add(faVideo);
+
+@observer
+export class ScreenshotBox extends DocAnnotatableComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) {
+    private _reactionDisposer?: IReactionDisposer;
+    private _videoRef: HTMLVideoElement | null = null;
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
+
+    public get player(): HTMLVideoElement | null {
+        return this._videoRef;
+    }
+
+    videoLoad = () => {
+        const aspect = this.player!.videoWidth / this.player!.videoHeight;
+        const nativeWidth = (this.Document._nativeWidth || 0);
+        const nativeHeight = (this.Document._nativeHeight || 0);
+        if (!nativeWidth || !nativeHeight) {
+            if (!this.Document._nativeWidth) this.Document._nativeWidth = this.player!.videoWidth;
+            this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect;
+            this.Document._height = (this.Document._width || 0) / aspect;
+        }
+        if (!this.Document.duration) this.Document.duration = this.player!.duration;
+    }
+
+    @action public Snapshot() {
+        const width = this.Document._width || 0;
+        const height = this.Document._height || 0;
+        const canvas = document.createElement('canvas');
+        canvas.width = 640;
+        canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1);
+        const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+        if (ctx) {
+            ctx.rect(0, 0, canvas.width, canvas.height);
+            ctx.fillStyle = "blue";
+            ctx.fill();
+            this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+        }
+
+        if (this._videoRef) {
+            //convert to desired file format
+            const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+            // if you want to preview the captured image,
+            const filename = path.basename(encodeURIComponent("screenshot" + Utils.GenerateGuid().replace(/\..*$/, "").replace(" ", "_")));
+            ScreenshotBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
+                setTimeout(() => {
+                    if (returnedFilename) {
+                        const imageSummary = Docs.Create.ImageDocument(Utils.prepend(returnedFilename), {
+                            x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
+                            _width: 150, _height: height / width * 150, title: "--screenshot--"
+                        });
+                        this.props.addDocument?.(imageSummary);
+                    }
+                }, 500);
+            });
+        }
+    }
+
+    componentDidMount() {
+    }
+
+    componentWillUnmount() {
+        this._reactionDisposer && this._reactionDisposer();
+    }
+
+    @action
+    setVideoRef = (vref: HTMLVideoElement | null) => {
+        this._videoRef = vref;
+    }
+
+    public static async convertDataUri(imageUri: string, returnedFilename: string) {
+        try {
+            const posting = Utils.prepend("/uploadURI");
+            const returnedUri = await rp.post(posting, {
+                body: {
+                    uri: imageUri,
+                    name: returnedFilename
+                },
+                json: true,
+            });
+            return returnedUri;
+
+        } catch (e) {
+            console.log(e);
+        }
+    }
+    @observable _screenCapture = false;
+    specificContextMenu = (e: React.MouseEvent): void => {
+        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+        if (field) {
+            const url = field.url.href;
+            const subitems: ContextMenuProps[] = [];
+            subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
+            subitems.push({
+                description: "Screen Capture", event: (async () => {
+                    runInAction(() => this._screenCapture = !this._screenCapture);
+                    this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+                }), icon: "expand-arrows-alt"
+            });
+            ContextMenu.Instance.addItem({ description: "Screenshot Funcs...", subitems: subitems, icon: "video" });
+        }
+    }
+
+    @computed get content() {
+        const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const style = "videoBox-content" + interactive;
+        return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+            style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
+            onCanPlay={this.videoLoad}
+            controls={true}
+            onClick={e => e.preventDefault()}>
+            <source type="video/mp4" />
+            Not supported.
+            </video>;
+    }
+
+    toggleRecording = action(async () => {
+        this._screenCapture = !this._screenCapture;
+        this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+    })
+
+    private get uIButtons() {
+        return ([
+            <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
+                <FontAwesomeIcon icon="file" size="lg" />
+            </div>,
+            <div className="screenshotBox-snapshot" key="snap" onPointerDown={this.onSnapshot} >
+                <FontAwesomeIcon icon="camera" size="lg" />
+            </div>]);
+    }
+
+    onSnapshot = (e: React.PointerEvent) => {
+        this.Snapshot();
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+
+    contentFunc = () => [this.content];
+    render() {
+        return (<div className="videoBox" onContextMenu={this.specificContextMenu}
+            style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
+            <div className="videoBox-viewer" >
+                <CollectionFreeFormView {...this.props}
+                    PanelHeight={this.props.PanelHeight}
+                    PanelWidth={this.props.PanelWidth}
+                    annotationsKey={this.annotationKey}
+                    focus={this.props.focus}
+                    isSelected={this.props.isSelected}
+                    isAnnotationOverlay={true}
+                    select={emptyFunction}
+                    active={this.annotationsActive}
+                    ContentScaling={returnOne}
+                    whenActiveChanged={this.whenActiveChanged}
+                    removeDocument={this.removeDocument}
+                    moveDocument={this.moveDocument}
+                    addDocument={returnFalse}
+                    CollectionView={undefined}
+                    ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                    renderDepth={this.props.renderDepth + 1}
+                    ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+                    {this.contentFunc}
+                </CollectionFreeFormView>
+            </div>
+            {this.uIButtons}
+        </div >);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 439f2d85f..24b66d8f7 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -195,6 +195,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             console.log(e);
         }
     }
+    @observable _screenCapture = false;
     specificContextMenu = (e: React.MouseEvent): void => {
         const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         if (field) {
@@ -203,6 +204,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
             subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" });
             subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
+            subitems.push({
+                description: "Screen Capture", event: (async () => {
+                    runInAction(() => this._screenCapture = !this._screenCapture);
+                    this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+                }), icon: "expand-arrows-alt"
+            });
             ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" });
         }
     }
@@ -212,8 +219,14 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
-            <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
-                onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}>
+            <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+                style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
+                onCanPlay={this.videoLoad}
+                controls={VideoBox._showControls}
+                onPlay={() => this.Play()}
+                onSeeked={this.updateTimecode}
+                onPause={() => this.Pause()}
+                onClick={e => e.preventDefault()}>
                 <source src={field.url.href} type="video/mp4" />
                 Not supported.
             </video>;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 226103a53..b41687c11 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,13 +1,18 @@
 @import "../globalCssVariables.scss";
 
+
+.webBox-container, .webBox-container-dragging {
+    transform-origin: top left;
+}
 .webBox-cont,
-.webBox-cont-interactive {
+.webBox-cont-dragging {
     padding: 0vw;
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
     height: 100%;
+    transform-origin: top left;
     overflow: auto;
     pointer-events: none;
 }
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 557adfc03..591864f2c 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -323,7 +323,14 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             </>);
     }
     render() {
-        return (<div className={"webBox-container"} >
+        const dragging = "";//</div>!SelectionManager.GetIsDragging() ? "" : "-dragging";
+        return (<div className={`webBox-container${dragging}`}
+            style={{
+                transform: `scale(${this.props.ContentScaling()})`,
+                width: `${100 / this.props.ContentScaling()}%`,
+                height: `${100 / this.props.ContentScaling()}%`,
+                pointerEvents: this.props.Document.isBackground ? "none" : undefined
+            }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index d1f68ba49..ea7b91b23 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -67,7 +67,8 @@ export class CurrentUserUtils {
             { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' },
             { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' },
             { title: "buxton", icon: "cloud-upload-alt", ignoreClick: true, drag: "Docs.Create.Buxton()" },
-            { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { width: 400, height: 400, title: "a test cam" })' },
+            { title: "screenshot", icon: "frame", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
+            { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
             { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
             { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' },
             { title: "presentation", icon: "tv", click: 'openOnRight(Doc.UserDoc().curPresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().curPresentation = getCopy(this.dragFactory,true)`, dragFactory: emptyPresentation },
-- 
cgit v1.2.3-70-g09d2


From f488ca2b44af291c7bea853ad574cf7453fadb25 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 28 Mar 2020 01:00:30 -0400
Subject: changed template parameters to be stored in '@params'.   Updated
 ImageBox to allow template parameters

---
 src/client/util/RichTextSchema.tsx  |  4 +-
 src/client/views/nodes/ImageBox.tsx | 86 +++++++++++++++++++------------------
 src/new_fields/Doc.ts               |  4 +-
 3 files changed, 50 insertions(+), 44 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 5b3119d38..a81c6515d 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -20,7 +20,6 @@ import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from
 import { DocServer } from "../DocServer";
 import { Docs } from "../documents/Documents";
 import { CollectionViewType } from "../views/collections/CollectionView";
-import { ContextMenu } from "../views/ContextMenu";
 import { DocumentView } from "../views/nodes/DocumentView";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 import { DocumentManager } from "./DocumentManager";
@@ -952,7 +951,8 @@ export class DashFieldView {
                 self._dashDoc[self._fieldKey] = StrCast(self._options[0].title);
             }
             const layout = tbox.props.Document;
-            self._fieldKey = self._fieldKey.startsWith("@") ? StrCast(layout[StrCast(self._fieldKey).substring(1)]) : self._fieldKey;
+            // NOTE: if the field key starts with "@", then the actual field key is stored in the "@"fieldKey.  Dereferencing these fields happens in ImageBox and RichTextSchema
+            self._fieldKey = self._fieldKey.startsWith("@") ? StrCast(layout[StrCast(self._fieldKey)]) : self._fieldKey;
             this._labelSpan.innerHTML = `${self._fieldKey}: `;
             const fieldVal = Cast(this._dashDoc?.[self._fieldKey], "boolean", null);
             this._fieldCheck.style.display = (fieldVal === true || fieldVal === false) ? "inline-block" : "none";
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b1c172e22..04ebf477b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -79,23 +79,27 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
     }
 
+    get fieldKey() {
+        return this.props.fieldKey.startsWith("@") ? StrCast(this.props.Document[this.props.fieldKey]) : this.props.fieldKey;
+    }
+
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.docDragData) {
             if (de.metaKey) {
                 de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => {
-                    Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-alternates", drop);
+                    Doc.AddDocToList(this.dataDoc, this.fieldKey + "-alternates", drop);
                     e.stopPropagation();
                 }));
-            } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) {
+            } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
                 const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
                 const targetField = Doc.LayoutFieldKey(layoutDoc);
                 const targetDoc = layoutDoc[DataSym];
                 if (targetDoc[targetField] instanceof ImageField) {
-                    this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
-                    this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]);
-                    this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(targetDoc[targetField + "-nativeHeight"]);
+                    this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
+                    this.dataDoc[this.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]);
+                    this.dataDoc[this.fieldKey + "-nativeHeight"] = NumCast(targetDoc[targetField + "-nativeHeight"]);
                     e.stopPropagation();
                 }
             }
@@ -123,9 +127,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 // upload to server with known URL 
                 const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 });
                 audioDoc.treeViewExpandedView = "layout";
-                const audioAnnos = Cast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"], listSpec(Doc));
+                const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc));
                 if (audioAnnos === undefined) {
-                    this.dataDoc[this.props.fieldKey + "-audioAnnotations"] = new List([audioDoc]);
+                    this.dataDoc[this.fieldKey + "-audioAnnotations"] = new List([audioDoc]);
                 } else {
                     audioAnnos.push(audioDoc);
                 }
@@ -142,33 +146,33 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @undoBatch
     rotate = action(() => {
-        const nw = NumCast(this.Document[this.props.fieldKey + "-nativeWidth"]);
-        const nh = NumCast(this.Document[this.props.fieldKey + "-nativeHeight"]);
+        const nw = NumCast(this.Document[this.fieldKey + "-nativeWidth"]);
+        const nh = NumCast(this.Document[this.fieldKey + "-nativeHeight"]);
         const w = this.Document._width;
         const h = this.Document._height;
-        this.dataDoc[this.props.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) + 90) % 360;
-        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = nh;
-        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = nw;
+        this.dataDoc[this.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.fieldKey + "-rotation"]) + 90) % 360;
+        this.dataDoc[this.fieldKey + "-nativeWidth"] = nh;
+        this.dataDoc[this.fieldKey + "-nativeHeight"] = nw;
         this.Document._width = h;
         this.Document._height = w;
     });
 
     specificContextMenu = (e: React.MouseEvent): void => {
-        const field = Cast(this.Document[this.props.fieldKey], ImageField);
+        const field = Cast(this.Document[this.fieldKey], ImageField);
         if (field) {
             const funcs: ContextMenuProps[] = [];
             funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
             funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
             funcs.push({
                 description: "Reset Native Dimensions", event: action(async () => {
-                    const curNW = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]);
-                    const curNH = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"]);
+                    const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]);
+                    const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]);
                     if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
-                        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
-                        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
+                        this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
+                        this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
                     } else {
-                        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
-                        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
+                        this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
+                        this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
                     }
                 }), icon: "expand-arrows-alt"
             });
@@ -190,7 +194,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
             return faceDocs;
         };
-        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-faces"], this.url, Service.Face, converter);
+        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter);
     }
 
     generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -202,16 +206,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 const sanitized = tag.name.replace(" ", "_");
                 tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
             });
-            this.dataDoc[this.props.fieldKey + "-generatedTags"] = tagsList;
+            this.dataDoc[this.fieldKey + "-generatedTags"] = tagsList;
             tagDoc.title = "Generated Tags Doc";
             tagDoc.confidence = threshold;
             return tagDoc;
         };
-        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
+        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
     }
 
     @computed private get url() {
-        const data = Cast(this.dataDoc[this.props.fieldKey], ImageField);
+        const data = Cast(this.dataDoc[this.fieldKey], ImageField);
         return data ? data.url.href : undefined;
     }
 
@@ -244,32 +248,32 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         }
         const original = StrCast(this.dataDoc.originalUrl);
         if (error.type === "error" && original) {
-            this.dataDoc[this.props.fieldKey] = new ImageField(original);
+            this.dataDoc[this.fieldKey] = new ImageField(original);
         }
     }
     _curSuffix = "_m";
 
     resize = (imgPath: string) => {
         const cachedNativeSize = {
-            width: NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]),
-            height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"])
+            width: NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]),
+            height: NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"])
         };
-        const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"];
+        const cachedImgPath = this.dataDoc[this.fieldKey + "-imgPath"];
         const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
         const cachedAspect = cachedNativeSize.height / cachedNativeSize.width;
         if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05 || imgPath !== cachedImgPath) {
             if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) {
                 requestImageSize(imgPath).then((inquiredSize: any) => {
-                    const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
+                    const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180;
                     const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
                     const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width;
                     setTimeout(action(() => {
                         if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) {
                             this.Document._height = this.Document[WidthSym]() * rotatedAspect;
-                            this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
-                            this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
+                            this.dataDoc[this.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
+                            this.dataDoc[this.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
                         }
-                        this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath;
+                        this.dataDoc[this.fieldKey + "-imgPath"] = imgPath;
                     }), 0);
                 }).catch((err: any) => console.log(err));
             } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) {
@@ -289,7 +293,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     @action
     onPointerEnter = () => {
         const self = this;
-        const audioAnnos = DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]);
+        const audioAnnos = DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]);
         if (audioAnnos && audioAnnos.length && this._audioState === 0) {
             const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
             anno.data instanceof AudioField && new Howl({
@@ -324,7 +328,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @computed
     private get considerDownloadIcon() {
-        const data = this.dataDoc[this.props.fieldKey];
+        const data = this.dataDoc[this.fieldKey];
         if (!(data instanceof ImageField)) {
             return (null);
         }
@@ -353,7 +357,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     setTimeout(action(() => {
                         this.uploadIcon = idle;
                         if (data) {
-                            dataDoc[this.props.fieldKey] = data;
+                            dataDoc[this.fieldKey] = data;
                         }
                     }), 2000);
                 }}
@@ -363,8 +367,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @computed get nativeSize() {
         const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
-        const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw);
-        const nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], 1);
+        const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], pw);
+        const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1);
         return { nativeWidth, nativeHeight };
     }
 
@@ -372,9 +376,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
-        const alts = DocListCast(this.dataDoc[this.props.fieldKey + "-alternates"]);
+        const alts = DocListCast(this.dataDoc[this.fieldKey + "-alternates"]);
         const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
-        const field = this.dataDoc[this.props.fieldKey];
+        const field = this.dataDoc[this.fieldKey];
         // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
         // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
         // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
@@ -389,7 +393,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         const srcpath = this.paths[NumCast(this.props.Document.curPage, 0)];
         const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
         const { nativeWidth, nativeHeight } = this.nativeSize;
-        const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]);
+        const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]);
         const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
         const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
 
@@ -419,8 +423,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
                 >
                     <FontAwesomeIcon className="imageBox-audioFont"
-                        style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
-                        icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
+                        style={{ color: [DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
+                        icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
                 </div>}
             {this.considerDownloadIcon}
             {this.considerGooglePhotosLink()}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 2a7b08d44..da723b43b 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -484,7 +484,9 @@ export namespace Doc {
                 if (!targetDoc[expandedLayoutFieldKey]) {
                     const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
                     newLayoutDoc.expandedTemplate = targetDoc;
-                    newLayoutDoc.params = templateParams?.match(/\(([a-zA-Z0-9_-]*)\)/)?.[1];
+                    // the template's parameters are stored in params which are derefenced to find
+                    // the actual field key where the template data is stored.  Currently this is only used in RichTextSchema's docView
+                    newLayoutDoc["@params"] = templateParams?.match(/\(([a-zA-Z0-9_-]*)\)/)?.[1];
                     targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
                     const dataDoc = Doc.GetProto(targetDoc);
                     newLayoutDoc.resolvedDataDoc = dataDoc;
-- 
cgit v1.2.3-70-g09d2


From 8475b309106d519a2d541b7cd61f22ac44b6392d Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 29 Mar 2020 13:27:36 -0400
Subject: more tweaks to embedded documents in text. fixes for expandedTemplate
 to show the original doc (not dataDoc).  got rid of imgPath in imageBox.

---
 src/client/util/RichTextSchema.tsx  | 9 ++++++---
 src/client/views/nodes/ImageBox.tsx | 4 +---
 src/new_fields/Doc.ts               | 6 +++---
 3 files changed, 10 insertions(+), 9 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 71d4530f2..7e4a095bc 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -768,7 +768,7 @@ export class DashDocView {
         };
         const alias = node.attrs.alias;
 
-        const docid = node.attrs.docid || tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
+        const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
         DocServer.GetRefField(docid + alias).then(async dashDoc => {
             if (!(dashDoc instanceof Doc)) {
                 alias && DocServer.GetRefField(docid).then(async dashDocBase => {
@@ -800,10 +800,13 @@ export class DashDocView {
         this._dashDoc = dashDoc;
         const self = this;
         const dashLayoutDoc = Doc.Layout(dashDoc);
-        const finalLayout = this._textBox.props.Document instanceof Doc && (Doc.expandTemplateLayout(dashLayoutDoc,
+        const finalLayout = node.attrs.docid ? dashDoc : this._textBox.props.Document instanceof Doc && (Doc.expandTemplateLayout(dashLayoutDoc,
             dashLayoutDoc !== dashDoc || !Doc.AreProtosEqual(this._textBox.dataDoc, this._textBox.props.Document) ? this._textBox.dataDoc : undefined, node.attrs.fieldKey));
         if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
         else {
+            if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+                finalLayout.expandedTemplate = dashDoc.aliasOf;
+            }
             const layoutKey = StrCast(finalLayout.layoutKey);
             const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
             if (finalLayout !== dashDoc && finalKey) {
@@ -820,7 +823,7 @@ export class DashDocView {
             }, { fireImmediately: true });
             ReactDOM.render(<DocumentView
                 Document={finalLayout}
-                DataDoc={!node.attrs.docid ? this._textBox.dataDoc : undefined}
+                DataDoc={Cast(finalLayout.resolvedDataDoc, Doc, null)}
                 LibraryPath={this._textBox.props.LibraryPath}
                 fitToBox={BoolCast(dashDoc._fitToBox)}
                 addDocument={returnFalse}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 04ebf477b..68eb4493a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -258,10 +258,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             width: NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]),
             height: NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"])
         };
-        const cachedImgPath = this.dataDoc[this.fieldKey + "-imgPath"];
         const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
         const cachedAspect = cachedNativeSize.height / cachedNativeSize.width;
-        if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05 || imgPath !== cachedImgPath) {
+        if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05) {
             if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) {
                 requestImageSize(imgPath).then((inquiredSize: any) => {
                     const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180;
@@ -273,7 +272,6 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                             this.dataDoc[this.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
                             this.dataDoc[this.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
                         }
-                        this.dataDoc[this.fieldKey + "-imgPath"] = imgPath;
                     }), 0);
                 }).catch((err: any) => console.log(err));
             } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) {
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 3ac297e94..0e3bab32a 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -472,7 +472,7 @@ export namespace Doc {
     // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
     //   layout_mytemplate(somparam=somearg).   
     // then any references to @someparam would be rewritten as accesses to 'somearg' on the expandedTemplate
-    export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
+    export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string, templateParent?: Doc) {
         if (!WillExpandTemplateLayout(templateLayoutDoc, targetDoc) || !targetDoc) return templateLayoutDoc;
 
         const templateField = StrCast(templateLayoutDoc.isTemplateForField);  // the field that the template renders
@@ -497,7 +497,7 @@ export namespace Doc {
                     // the template's arguments are stored in params which is derefenced to find
                     // the actual field key where the parameterized template data is stored.
                     newLayoutDoc[params] = args;
-                    newLayoutDoc.expandedTemplate = targetDoc;
+                    newLayoutDoc.expandedTemplate = templateParent || targetDoc;
                     targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
                     const dataDoc = Doc.GetProto(targetDoc);
                     newLayoutDoc.resolvedDataDoc = dataDoc;
@@ -519,7 +519,7 @@ export namespace Doc {
         }
         const existingResolvedDataDoc = childDoc[DataSym] !== Doc.GetProto(childDoc)[DataSym] && childDoc[DataSym];
         const resolvedDataDoc = existingResolvedDataDoc || (Doc.AreProtosEqual(containerDataDoc, containerDoc) || !containerDataDoc || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc);
-        return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, "(" + StrCast(containerDoc["PARAMS"]) + ")"), data: resolvedDataDoc };
+        return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, "(" + StrCast(containerDoc["PARAMS"]) + ")", Cast(containerDoc.expandedTemplate, Doc, null)), data: resolvedDataDoc };
     }
 
     export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
-- 
cgit v1.2.3-70-g09d2


From bb351bcf0a56be9708c6c833c8023300e26223c1 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 2 Apr 2020 17:24:19 -0400
Subject: added copy with alt+ctrl .  fixed border radius

---
 src/client/util/DragManager.ts           | 2 +-
 src/client/views/DocumentDecorations.tsx | 2 +-
 src/client/views/nodes/ImageBox.tsx      | 3 ++-
 src/client/views/search/SearchBox.tsx    | 2 +-
 src/new_fields/Doc.ts                    | 3 ++-
 5 files changed, 7 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index db7a54ca8..1f2312032 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -351,7 +351,7 @@ export namespace DragManager {
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (dragData instanceof DocumentDragData) {
-                dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
+                dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
             }
             if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) {
                 AbortDrag();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3e07fd109..79600b7c1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -266,7 +266,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1]));
         dist = dist < 3 ? 0 : dist;
         SelectionManager.SelectedDocuments().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)).
-            map(d => d.borderRounding = `${Math.max(0, dist)}%`);
+            map(d => d.borderRounding = `${Math.max(0, dist)}px`);
         return false;
     }
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 68eb4493a..e710b3075 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -439,7 +439,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 transform: `scale(${this.props.ContentScaling()})`,
                 width: `${100 / this.props.ContentScaling()}%`,
                 height: `${100 / this.props.ContentScaling()}%`,
-                pointerEvents: this.props.Document.isBackground ? "none" : undefined
+                pointerEvents: this.props.Document.isBackground ? "none" : undefined,
+                borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px`
             }} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 9bd42b516..586365f6e 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -347,7 +347,7 @@ export class SearchBox extends React.Component {
                     <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
                         className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch}
                         style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
-                    <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
+                    <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={() => { }} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
                 </div>
                 <div className="searchBox-quickFilter" onPointerDown={this.openSearch}>
                     <div className="filter-panel"><IconBar /></div>
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index a6cbabf42..85926e393 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -565,7 +565,8 @@ export namespace Doc {
                 } else if (cfield instanceof ComputedField) {
                     copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
                 } else if (field instanceof ObjectField) {
-                    copy[key] = key.includes("layout[") && doc[key] instanceof Doc ? Doc.MakeCopy(doc[key] as Doc, false) : ObjectField.MakeCopy(field);
+                    copy[key] = key.includes("layout[") && doc[key] instanceof Doc ? Doc.MakeCopy(doc[key] as Doc, false) :
+                        doc[key] instanceof Doc ? doc[key] : ObjectField.MakeCopy(field);
                 } else if (field instanceof Promise) {
                     debugger; //This shouldn't happend...
                 } else {
-- 
cgit v1.2.3-70-g09d2


From b4958eac84339dd7a88c964a9c52e89481048f55 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 2 Apr 2020 19:39:03 -0400
Subject: supported uploads of images ending with .webp

---
 src/client/views/collections/CollectionView.tsx | 6 +++---
 src/client/views/nodes/ImageBox.tsx             | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index b6ce2f3a9..df1770ffe 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -429,9 +429,9 @@ export class CollectionView extends Touchable<FieldViewProps> {
                         Document={facetCollection}
                         backgroundColor={this.filterBackground}
                         fieldKey={`${this.props.fieldKey}-filter`}
-                        moveDocument={(doc: Doc) => false}
-                        removeDocument={(doc: Doc) => false}
-                        addDocument={(doc: Doc) => false} />
+                        moveDocument={returnFalse}
+                        removeDocument={returnFalse}
+                        addDocument={returnFalse} />
                 </div>
             </div>;
     }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index e710b3075..00057055f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -225,7 +225,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             return url.href;
         } else if (url.href.indexOf(window.location.origin) === -1) {
             return Utils.CorsProxy(url.href);
-        } else if (!/\.(png|jpg|jpeg|gif)$/.test(lower)) {
+        } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) {
             return url.href;//Why is this here
         }
         const ext = path.extname(url.href);
-- 
cgit v1.2.3-70-g09d2

