From 1a3f820a38a12b13e947f58b4b7459ed84c4f70b Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 19 Oct 2019 11:51:34 -0400
Subject: fixing up audioboxes little by little.

---
 src/client/views/nodes/AudioBox.tsx | 23 +++++++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index be6ae630f..689d44a2f 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -4,22 +4,37 @@ import { observer } from "mobx-react";
 import "./AudioBox.scss";
 import { Cast } from "../../../new_fields/Types";
 import { AudioField } from "../../../new_fields/URLField";
+import { DocStaticComponent } from "../DocComponent";
+import { makeInterface } from "../../../new_fields/Schema";
+import { documentSchema } from "./DocumentView";
+import { InkingControl } from "../InkingControl";
 
+type AudioDocument = makeInterface<[typeof documentSchema]>;
+const AudioDocument = makeInterface(documentSchema);
 const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
+
 @observer
-export class AudioBox extends React.Component<FieldViewProps> {
+export class AudioBox extends DocStaticComponent<FieldViewProps, AudioDocument>(AudioDocument) {
 
     public static LayoutString() { return FieldView.LayoutString(AudioBox); }
+    _ref = React.createRef<HTMLAudioElement>();
+
+    componentDidMount() {
+        if (this._ref.current) this._ref.current.currentTime = 1;
+    }
 
     render() {
         let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
         let path = field.url.href;
 
+        let interactive = this.active() ? "-interactive" : "";
         return (
-            <audio controls className="audiobox-cont" style={{ pointerEvents: "all" }}>
-                <source src={path} type="audio/mpeg" />
-                Not supported.
+            <div className="audiobox-container">
+                <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
+                    <source src={path} type="audio/mpeg" />
+                    Not supported.
                 </audio>
+            </div>
         );
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 4d02c9b581a22da777232124f2b1a96f8e342285 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 21 Oct 2019 15:45:51 -0400
Subject: initial simplification to fieldExt

---
 src/client/views/DocComponent.tsx                  | 20 +++---
 .../views/collections/CollectionBaseView.tsx       |  4 +-
 .../views/collections/CollectionSchemaView.tsx     |  4 +-
 .../views/collections/CollectionStackingView.tsx   |  2 +-
 .../CollectionStackingViewFieldColumn.tsx          |  4 +-
 src/client/views/collections/CollectionSubView.tsx |  3 +-
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../views/collections/CollectionViewChromes.tsx    | 72 ++--------------------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 13 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/nodes/AudioBox.tsx                |  1 -
 src/client/views/nodes/ColorBox.tsx                | 19 +-----
 src/client/views/nodes/DocumentView.tsx            |  2 +
 src/client/views/nodes/FormattedTextBox.tsx        |  6 --
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  5 +-
 src/client/views/nodes/WebBox.tsx                  |  2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  3 +-
 src/new_fields/Doc.ts                              | 69 +++++++++------------
 21 files changed, 74 insertions(+), 167 deletions(-)

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

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index ff149a9ac..1f9bdaac4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -10,6 +10,8 @@ import { InkTool } from '../../new_fields/InkField';
 ///  DocComponents returns a generic base class for React views of document fields that are not interactive
 interface DocComponentProps {
     Document: Doc;
+    DataDoc?: Doc;
+    fieldKey: string;
 }
 export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
@@ -18,6 +20,8 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
         get Document(): T {
             return schemaCtor(this.props.Document);
         }
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
     }
     return Component;
 }
@@ -26,6 +30,8 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
 ///  DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox)
 interface DocStaticProps {
     Document: Doc;
+    DataDoc?: Doc;
+    fieldKey: string;
     isSelected: () => boolean;
     renderDepth: number;
 }
@@ -36,6 +42,8 @@ export function DocStaticComponent<P extends DocStaticProps, T>(schemaCtor: (doc
         get Document(): T {
             return schemaCtor(this.props.Document);
         }
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
         active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
     }
     return Component;
@@ -54,12 +62,15 @@ interface DocAnnotatableProps {
 }
 export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
+        _isChildActive = false;
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
         @computed
         get Document(): T {
             return schemaCtor(this.props.Document);
         }
-        _isChildActive = false;
+        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
@@ -67,23 +78,18 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
             let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
             return index !== -1 && value.splice(index, 1) ? true : false;
         }
-
-        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
-
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
         // if the moved document is already in this overlay collection nothing needs to be done.
         // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. 
         @action.bound
         moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
             return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false;
         }
-
         @action.bound
         addDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = this.props.Document;
             return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc);
         }
+
         whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
         active = () => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
             (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false)
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 1ade44250..fa543cc01 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -97,9 +97,11 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
 
     @action.bound
     addDocument(doc: Doc): boolean {
-        let targetDataDoc = this.props.Document;
+        let targetDataDoc = Doc.GetProto(this.props.Document);
         let targetField = this.props.fieldKey;
         Doc.AddDocToList(targetDataDoc, targetField, doc);
+        let extension = Doc.fieldExtensionDoc(targetDataDoc, targetField);
+        extension && (extension.lastModified = new DateField(new Date(Date.now())));
         Doc.GetProto(doc).lastOpened = new DateField;
         return true;
     }
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 23e070750..04bc550ea 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -158,6 +158,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
             <CollectionSchemaPreview
                 Document={layoutDoc}
                 DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+                fieldKey={this.props.fieldKey}
                 childDocs={this.childDocs}
                 renderDepth={this.props.renderDepth}
                 ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
@@ -226,7 +227,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
     }
 
     render() {
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         return (
             <div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}>
                 <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
@@ -898,6 +898,7 @@ interface CollectionSchemaPreviewProps {
     childDocs?: Doc[];
     renderDepth: number;
     fitToBox?: boolean;
+    fieldKey: string;
     PanelWidth: () => number;
     PanelHeight: () => number;
     ruleProvider: Doc | undefined;
@@ -993,6 +994,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                     <DocumentView {...this.props}
                         DataDoc={this.props.DataDocument}
                         Document={this.props.Document}
+                        fieldKey={this.props.fieldKey}
                         fitToBox={this.props.fitToBox}
                         onClick={this.props.onClick}
                         ruleProvider={this.props.ruleProvider}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 1bb2d2f46..5fb0c0f9f 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -170,6 +170,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         return <CollectionSchemaPreview
             Document={doc}
             DataDocument={dataDoc}
+            fieldKey={this.props.fieldKey}
             showOverlays={this.overlays}
             renderDepth={this.props.renderDepth}
             ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
@@ -389,7 +390,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             SetValue: this.addGroup,
             contents: "+ ADD A GROUP"
         };
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
         if (this.sectionFilter) {
             let entries = Array.from(this.Sections.entries());
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 7e54b0f29..fec3d90b9 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,7 +2,7 @@ import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPalette } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, trace } from "mobx";
+import { action, observable, trace, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, WidthSym } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
@@ -204,7 +204,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             document.removeEventListener("pointerup", this.pointerUp);
             document.addEventListener("pointerup", this.pointerUp);
         }
-        this._createAliasSelected = false;
+        runInAction(() => this._createAliasSelected = false);
     }
 
     renderColorPicker = () => {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 8bd67b880..43147ed20 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -33,6 +33,7 @@ export interface CollectionViewProps extends FieldViewProps {
     VisibleHeight?: () => number;
     chromeCollapsed: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+    fieldKey: string;
 }
 
 export interface SubCollectionViewProps extends CollectionViewProps {
@@ -77,7 +78,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         // to its children which may be templates.
         // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise.
         @computed get dataField() {
-            return Doc.fieldExtensionDoc(this.props.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey];
+            return this.props.fieldExt ? this.extensionDoc[this.props.fieldExt] : this.dataDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs() {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c1b7f7e48..2cad41acb 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -317,6 +317,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 <CollectionSchemaPreview
                     Document={layoutDoc}
                     DataDocument={this.templateDataDoc}
+                    fieldKey={this.fieldKey}
                     renderDepth={this.props.renderDepth}
                     showOverlays={this.noOverlays}
                     ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider}
@@ -556,7 +557,6 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
 
     render() {
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
         let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
         let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 4ca6df034..74a388425 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -149,7 +149,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
                 {this.SubView}
             </CollectionBaseView>
 
-            {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField)!.url!.href))}
+            {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField)!.url.href))}
         </>
         );
     }
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index a5b7f0181..dd5e630e4 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -480,12 +480,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
 
     getKeySuggestions = async (value: string): Promise<string[]> => {
         value = value.toLowerCase();
-        let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
-            = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
-        if (typeof docs === "function") {
-            docs = docs();
-        }
-        docs = await docs;
+        let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
         if (docs instanceof Doc) {
             return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
         } else {
@@ -591,19 +586,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
         if (textwrappedRows.length) {
             this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
         } else {
-            let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
-                = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
-            if (typeof docs === "function") {
-                docs = docs();
-            }
-            docs = await docs;
-            if (docs instanceof Doc) {
-                let allRows = [docs[Id]];
-                this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
-            } else {
-                let allRows = docs.map(doc => doc[Id]);
-                this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
-            }
+            let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
+            let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+            this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
         }
     }
 
@@ -638,63 +623,14 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
 
 @observer
 export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
-    @observable private _currentKey: string = "";
-    @observable private suggestions: string[] = [];
 
     @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); }
-    @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
-
-    getKeySuggestions = async (value: string): Promise<string[]> => {
-        value = value.toLowerCase();
-        let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
-            = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
-        if (typeof docs === "function") {
-            docs = docs();
-        }
-        docs = await docs;
-        if (docs instanceof Doc) {
-            return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
-        } else {
-            const keys = new Set<string>();
-            docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
-            return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
-        }
-    }
-
-    @action
-    onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
-        this._currentKey = newValue;
-    }
-
-    getSuggestionValue = (suggestion: string) => suggestion;
-
-    renderSuggestion = (suggestion: string) => {
-        return <p>{suggestion}</p>;
-    }
-
-    onSuggestionFetch = async ({ value }: { value: string }) => {
-        const sugg = await this.getKeySuggestions(value);
-        runInAction(() => {
-            this.suggestions = sugg;
-        });
-    }
-
-    @action
-    onSuggestionClear = () => {
-        this.suggestions = [];
-    }
-
-    setValue = (value: string) => {
-        this.props.CollectionView.props.Document.sectionFilter = value;
-        return true;
-    }
 
     @action toggleSort = () => {
         if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined;
         else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false;
         else this.props.CollectionView.props.Document.sortAscending = true;
     }
-    @action resetValue = () => { this._currentKey = this.sectionFilter; };
 
     render() {
         return (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 430ba582b..33d6b1358 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -113,10 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     }
 
-    @computed get fieldExtensionDoc() {
-        return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
-    }
-
     @action
     onDrop = (e: React.DragEvent): Promise<void> => {
         var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
@@ -309,7 +305,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
                     [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
                 }, [[minx, maxx], [miny, maxy]]);
-                let ink = Cast(this.fieldExtensionDoc.ink, InkField);
+                let ink = Cast(this.extensionDoc.ink, InkField);
                 if (ink && ink.inkData) {
                     ink.inkData.forEach((value: StrokeData, key: string) => {
                         let bounds = InkingCanvas.StrokeRect(value);
@@ -605,9 +601,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     analyzeStrokes = async () => {
-        let data = Cast(this.fieldExtensionDoc.ink, InkField);
+        let data = Cast(this.extensionDoc.ink, InkField);
         if (data) {
-            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
+            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
         }
     }
 
@@ -680,7 +676,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
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey);
         return (
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
@@ -690,7 +685,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
-                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
+                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
                             {this.childViews}
                         </InkingCanvas>
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 743055875..4ff70daba 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -247,12 +247,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
 
     get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
         let cprops = this.props.container.props;
-        return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField);
+        return Cast(this.props.container.extensionDoc.ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
         let cprops = this.props.container.props;
-        Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
+        this.props.container.extensionDoc.ink = value;
     }
 
     @undoBatch
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 689d44a2f..3e5deb55b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,7 +7,6 @@ import { AudioField } from "../../../new_fields/URLField";
 import { DocStaticComponent } from "../DocComponent";
 import { makeInterface } from "../../../new_fields/Schema";
 import { documentSchema } from "./DocumentView";
-import { InkingControl } from "../InkingControl";
 
 type AudioDocument = makeInterface<[typeof documentSchema]>;
 const AudioDocument = makeInterface(documentSchema);
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 30554ea36..fdcedb3a5 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -11,7 +11,6 @@ import { trace, reaction, observable, action, IReactionDisposer } from "mobx";
 import { SelectionManager } from "../../util/SelectionManager";
 import { StrCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { Doc } from "../../../new_fields/Doc";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
@@ -19,8 +18,11 @@ const ColorDocument = makeInterface(documentSchema);
 @observer
 export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+
     _selectedDisposer: IReactionDisposer | undefined;
     _penDisposer: IReactionDisposer | undefined;
+    @observable _startupColor = "black";
+
     componentDidMount() {
         this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"),
@@ -28,27 +30,12 @@ export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(
         this._penDisposer = reaction(() => CurrentUserUtils.ActivePen,
             action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"),
             { fireImmediately: true });
-
-        // compare to this reaction that used to be in Selection Manager
-        // reaction(() => manager.SelectedDocuments, sel => {
-        //     let targetColor = "#FFFFFF";
-        //     if (sel.length > 0) {
-        //         let firstView = sel[0];
-        //         let doc = firstView.props.Document;
-        //         let targetDoc = doc.isTemplateField ? doc : Doc.GetProto(doc);
-        //         let stored = StrCast(targetDoc.backgroundColor);
-        //         stored.length > 0 && (targetColor = stored);
-        //     }
-        //     InkingControl.Instance.updateSelectedColor(targetColor);
-        // }, { fireImmediately: true });
     }
     componentWillUnmount() {
         this._penDisposer && this._penDisposer();
         this._selectedDisposer && this._selectedDisposer();
     }
 
-    @observable _startupColor = "black";
-
     render() {
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
             onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a41a37e66..089ec77ba 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -67,6 +67,7 @@ library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCom
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView>;
     ContainingCollectionDoc: Opt<Doc>;
+    fieldKey: string;
     Document: Doc;
     DataDoc?: Doc;
     fitToBox?: boolean;
@@ -598,6 +599,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
             Document={this.props.Document}
+            fieldKey={this.props.fieldKey}
             fitToBox={this.props.fitToBox}
             addDocument={this.props.addDocument}
             removeDocument={this.props.removeDocument}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f866e6f7e..020442bf3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -44,7 +44,6 @@ import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { siteVerification } from 'googleapis/build/src/apis/siteVerification';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -143,10 +142,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
 
-    @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
-    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
-
     // the document containing the view layout information - will be the Document itself unless the Document has
     // a layout field.  In that case, all layout information comes from there unless overriden by Document
     @computed get layoutDoc(): Doc { return Doc.Layout(this.props.Document); }
@@ -1028,7 +1023,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
         let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground
             ? "none" : "all";
-        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         if (this.props.isSelected()) {
             FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
         }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 426b9cbb4..d80e222c2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -326,11 +326,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     render() {
-        if (!Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey)) return (null);
         return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
+                fieldExt={"annotations"}
                 isAnnotationOverlay={true}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5795d6f12..78858731f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -229,7 +229,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
                     renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
                     Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
                     addDocTab={this.props.addDocTab} GoToPage={this.gotoPage} focus={this.props.focus}
-                    pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
+                    pinToPres={this.props.pinToPres} addDocument={this.addDocument}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                     isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
                     fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 20383bab1..5e8154233 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -322,7 +322,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         InkingControl.Instance.switchTool(InkTool.None);
         this._isResetClick < 10 && (this.Document.currentTimecode = 0);
     }
-    @computed get fieldExtensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
     @computed get dataDoc() { return this.props.DataDoc && this.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
     @computed get youtubeContent() {
@@ -337,14 +336,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     @action.bound
     addDocumentWithTimestamp(doc: Doc): boolean {
-        Doc.GetProto(doc).annotationOn = this.props.Document;
         var curTime = (this.Document.currentTimecode || -1);
         curTime !== -1 && (doc.displayTimecode = curTime);
-        return Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, doc);
+        return this.addDocument(doc);
     }
 
     render() {
-        if (!Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey)) return (null);
         return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d5a1f4e73..907e71627 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -195,11 +195,11 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             </>);
     }
     render() {
-        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         return (<div className={"imageBox-container"} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
+                fieldExt={"annotations"}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index f3aaca471..361e58c65 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -46,7 +46,6 @@ interface IViewerProps {
     pdf: Pdfjs.PDFDocumentProxy;
     url: string;
     fieldKey: string;
-    fieldExt: string;
     Document: Doc;
     DataDoc?: Doc;
     ContainingCollectionView: Opt<CollectionView>;
@@ -550,7 +549,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     highlight = (color: string) => {
         // creates annotation documents for current highlights
         let annotationDoc = this.makeAnnotationDocument(color);
-        annotationDoc && Doc.AddDocToList(this.props.extensionDoc, this.props.fieldExt, annotationDoc);
+        annotationDoc && this.props.addDocument && this.props.addDocument(annotationDoc);
         return annotationDoc;
     }
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 71b64d7c3..f60a2e720 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -398,44 +398,6 @@ export namespace Doc {
         return bounds;
     }
 
-    //
-    // Resolves a reference to a field by returning 'doc' if no field extension is specified,
-    // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
-    // This mechanism allows any fields to be extended with an extension document that can
-    // be used to capture field-specific metadata.  For example, an image field can be extended
-    // to store annotations, ink, and other data.
-    //
-    export function fieldExtensionDoc(doc: Doc, fieldKey: string, fieldExt: string = "yes") {
-        return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
-    }
-
-    export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-        let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
-        docExtensionForField.title = fieldKey + ".ext";
-        docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
-        docExtensionForField.type = DocumentType.EXTENSION;
-        let proto: Doc | undefined = doc;
-        while (proto && !Doc.IsPrototype(proto) && proto.proto) {
-            proto = proto.proto;
-        }
-        (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
-        return docExtensionForField;
-    }
-
-    export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
-        let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
-        if (docExtensionForField === undefined) {
-            if (immediate) {
-                CreateDocumentExtensionForField(doc, fieldKey);
-                return true;
-            }
-            else {
-                setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
-                return false;
-            }
-        }
-        return true;
-    }
     export function MakeTitled(title: string) {
         let doc = new Doc();
         doc.title = title;
@@ -490,13 +452,38 @@ export namespace Doc {
         let layoutDoc: Doc | undefined = childDocLayout;
         let resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
         if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
-            Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
-            let fieldExtensionDoc = Doc.fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
-            layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined);
+            let extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
+            layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
         } else layoutDoc = childDocLayout;
         return { layout: layoutDoc, data: resolvedDataDoc };
     }
 
+    //
+    // Resolves a reference to a field by returning 'doc' if no field extension is specified,
+    // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
+    // This mechanism allows any fields to be extended with an extension document that can
+    // be used to capture field-specific metadata.  For example, an image field can be extended
+    // to store annotations, ink, and other data.
+    //
+    export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
+        let extension = doc[fieldKey + "_ext"] as Doc;
+        (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+        return extension ? extension : doc;
+    }
+
+    export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+        let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+        docExtensionForField.title = fieldKey + ".ext";
+        docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
+        docExtensionForField.type = DocumentType.EXTENSION;
+        let proto: Doc | undefined = doc;
+        while (proto && !Doc.IsPrototype(proto) && proto.proto) {
+            proto = proto.proto;
+        }
+        (proto ? 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 => {
             const field = ProxyField.WithoutProxy(() => doc[key]);
-- 
cgit v1.2.3-70-g09d2


From 891b9706ddabc0a73ea6b25dc504297d6efb90fe Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 21 Oct 2019 22:03:02 -0400
Subject: big cleanup of layoutStrings, fieldExt, fieldKey, etc

---
 src/client/apis/youtube/YoutubeBox.tsx             |   2 +-
 src/client/documents/Documents.ts                  |  55 ++++----
 src/client/northstar/dash-nodes/HistogramBox.tsx   |   2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   2 +-
 src/client/views/CollectionLinearView.tsx          |   4 +-
 src/client/views/DocComponent.tsx                  |  45 +++----
 src/client/views/DocumentDecorations.tsx           |  13 +-
 src/client/views/MainView.tsx                      |   5 +-
 .../views/collections/CollectionSchemaView.tsx     |   1 -
 .../views/collections/CollectionStackingView.tsx   |   2 -
 src/client/views/collections/CollectionSubView.tsx |   9 +-
 src/client/views/collections/CollectionView.tsx    |  18 +--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  15 +--
 .../collections/collectionFreeForm/MarqueeView.tsx |  24 ++--
 src/client/views/linking/LinkFollowBox.tsx         |   2 +-
 src/client/views/nodes/AudioBox.tsx                |   8 +-
 src/client/views/nodes/ButtonBox.tsx               |  20 +--
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  18 +--
 src/client/views/nodes/ColorBox.tsx                |  10 +-
 src/client/views/nodes/DocuLinkBox.tsx             |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  71 ++--------
 src/client/views/nodes/FieldView.tsx               |   5 +-
 src/client/views/nodes/FontIconBox.tsx             |   2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  27 ++--
 src/client/views/nodes/IconBox.tsx                 |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  18 +--
 src/client/views/nodes/KeyValueBox.tsx             |   5 +-
 src/client/views/nodes/PDFBox.tsx                  |   8 +-
 src/client/views/nodes/PresBox.tsx                 |   2 +-
 src/client/views/nodes/QueryBox.tsx                |   2 +-
 src/client/views/nodes/VideoBox.tsx                |  40 +++---
 src/client/views/nodes/WebBox.tsx                  |   9 +-
 src/client/views/pdf/PDFViewer.tsx                 |  17 ++-
 .../views/presentationview/PresElementBox.tsx      | 148 ++++++++++-----------
 src/new_fields/Doc.ts                              |   3 +-
 src/new_fields/documentSchemas.ts                  |  51 +++++++
 36 files changed, 319 insertions(+), 348 deletions(-)
 create mode 100644 src/new_fields/documentSchemas.ts

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

diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index d73988bb8..bed812852 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -40,7 +40,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
     @observable curVideoTemplates: VideoTemplate[] = [];
 
 
-    public static LayoutString() { return FieldView.LayoutString(YoutubeBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(YoutubeBox, fieldKey); }
 
     /**
      * When component mounts, last search's results are laoded in based on the back up stored
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 937d3c058..a400e68a3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -84,7 +84,8 @@ export interface DocumentOptions {
     columnWidth?: number;
     fontSize?: number;
     curPage?: number;
-    currentTimecode?: number;
+    currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)
+    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;
@@ -119,11 +120,11 @@ export namespace Docs {
 
     export namespace Prototypes {
 
-        type LayoutSource = { LayoutString: (ext?: string) => string };
+        type LayoutSource = { LayoutString: (key: string) => string };
         type PrototypeTemplate = {
             layout: {
                 view: LayoutSource,
-                ext?: string, // optional extension field for layout source
+                dataField: string
             },
             options?: Partial<DocumentOptions>
         };
@@ -133,80 +134,80 @@ export namespace Docs {
 
         const TemplateMap: TemplateMap = new Map([
             [DocumentType.TEXT, {
-                layout: { view: FormattedTextBox },
+                layout: { view: FormattedTextBox, dataField: data },
                 options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
             }],
             [DocumentType.HIST, {
-                layout: { view: HistogramBox },
+                layout: { view: HistogramBox, dataField: data },
                 options: { height: 300, backgroundColor: "black" }
             }],
             [DocumentType.QUERY, {
-                layout: { view: QueryBox },
+                layout: { view: QueryBox, dataField: data },
                 options: { width: 400 }
             }],
             [DocumentType.COLOR, {
-                layout: { view: ColorBox },
+                layout: { view: ColorBox, dataField: data },
                 options: { nativeWidth: 220, nativeHeight: 300 }
             }],
             [DocumentType.IMG, {
-                layout: { view: ImageBox },
+                layout: { view: ImageBox, dataField: data },
                 options: {}
             }],
             [DocumentType.WEB, {
-                layout: { view: WebBox },
+                layout: { view: WebBox, dataField: data },
                 options: { height: 300 }
             }],
             [DocumentType.COL, {
-                layout: { view: CollectionView },
+                layout: { view: CollectionView, dataField: data },
                 options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
             }],
             [DocumentType.KVP, {
-                layout: { view: KeyValueBox },
+                layout: { view: KeyValueBox, dataField: data },
                 options: { height: 150 }
             }],
             [DocumentType.VID, {
-                layout: { view: VideoBox },
+                layout: { view: VideoBox, dataField: data },
                 options: { currentTimecode: 0 },
             }],
             [DocumentType.AUDIO, {
-                layout: { view: AudioBox },
+                layout: { view: AudioBox, dataField: data },
                 options: { height: 32 }
             }],
             [DocumentType.PDF, {
-                layout: { view: PDFBox },
+                layout: { view: PDFBox, dataField: data },
                 options: { nativeWidth: 1200, curPage: 1 }
             }],
             [DocumentType.ICON, {
-                layout: { view: IconBox },
+                layout: { view: IconBox, dataField: data },
                 options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
             }],
             [DocumentType.IMPORT, {
-                layout: { view: DirectoryImportBox },
+                layout: { view: DirectoryImportBox, dataField: data },
                 options: { height: 150 }
             }],
             [DocumentType.LINKDOC, {
                 data: new List<Doc>(),
-                layout: { view: EmptyBox },
+                layout: { view: EmptyBox, dataField: data },
             }],
             [DocumentType.YOUTUBE, {
-                layout: { view: YoutubeBox }
+                layout: { view: YoutubeBox, dataField: data }
             }],
             [DocumentType.BUTTON, {
-                layout: { view: ButtonBox },
+                layout: { view: ButtonBox, dataField: data },
             }],
             [DocumentType.PRES, {
-                layout: { view: PresBox },
+                layout: { view: PresBox, dataField: data },
                 options: {}
             }],
             [DocumentType.FONTICON, {
-                layout: { view: FontIconBox },
+                layout: { view: FontIconBox, dataField: data },
                 options: { width: 40, height: 40, borderRounding: "100%" },
             }],
             [DocumentType.LINKFOLLOW, {
-                layout: { view: LinkFollowBox }
+                layout: { view: LinkFollowBox, dataField: data }
             }],
             [DocumentType.PRESELEMENT, {
-                layout: { view: PresElementBox }
+                layout: { view: PresElementBox, dataField: data }
             }],
         ]);
 
@@ -285,7 +286,7 @@ export namespace Docs {
             // synthesize the default options, the type and title from computed values and
             // whatever options pertain to this specific prototype
             let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
-            options.layout = layout.view.LayoutString();
+            options.layout = layout.view.LayoutString(layout.dataField);
             return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: options.layout });
         }
 
@@ -701,12 +702,12 @@ export namespace DocUtils {
             linkDocProto.linkDescription = description;
 
             linkDocProto.anchor1 = source.doc;
-            linkDocProto.anchor1Context = source.ctx;
-            linkDocProto.anchor1Timecode = source.doc.currentTimecode;
-            linkDocProto.anchor1Groups = new List<Doc>([]);
             linkDocProto.anchor2 = target.doc;
+            linkDocProto.anchor1Context = source.ctx;
             linkDocProto.anchor2Context = target.ctx;
+            linkDocProto.anchor1Groups = new List<Doc>([]);
             linkDocProto.anchor2Groups = new List<Doc>([]);
+            linkDocProto.anchor1Timecode = source.doc.currentTimecode;
             linkDocProto.anchor2Timecode = target.doc.currentTimecode;
             linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
             linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index b81eafbee..854135648 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -24,7 +24,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 
 @observer
 export class HistogramBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(HistogramBox, fieldStr); }
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(HistogramBox, fieldStr); }
     private _dropXRef = React.createRef<HTMLDivElement>();
     private _dropYRef = React.createRef<HTMLDivElement>();
     private _dropXDisposer?: DragManager.DragDropDisposer;
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d74b51993..f27d05487 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -50,7 +50,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     @observable private uploading = false;
     @observable private removeHover = false;
 
-    public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
 
     constructor(props: FieldViewProps) {
         super(props);
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 4e03b8c95..1f28ef35d 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -10,7 +10,8 @@ import { Transform } from '../util/Transform';
 import "./CollectionLinearView.scss";
 import { CollectionViewType } from './collections/CollectionBaseView';
 import { CollectionSubView } from './collections/CollectionSubView';
-import { documentSchema, DocumentView } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
+import { documentSchema } from '../../new_fields/documentSchemas';
 
 
 type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -75,7 +76,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                             <DocumentView
                                 Document={pair.layout}
                                 DataDoc={pair.data}
-                                fieldKey={this.props.fieldKey}
                                 addDocument={this.props.addDocument}
                                 moveDocument={this.props.moveDocument}
                                 addDocTab={this.props.addDocTab}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 2f93d9584..ae4b7cf3a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,48 +1,40 @@
 import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
 import { computed, action } from 'mobx';
-import { Cast, BoolCast } from '../../new_fields/Types';
+import { Cast } from '../../new_fields/Types';
 import { listSpec } from '../../new_fields/Schema';
 import { InkingControl } from './InkingControl';
 import { InkTool } from '../../new_fields/InkField';
+import { PositionDocument } from '../../new_fields/documentSchemas';
 
 
-///  DocComponents returns a generic base class for React views of document fields that are not interactive
+///  DocComponent returns a generic React base class used by views that don't have any data extensions (e.g.,CollectionFreeFormDocumentView, DocumentView, ButtonBox)
 interface DocComponentProps {
     Document: Doc;
-    DataDoc?: Doc;
-    fieldKey: string;
 }
 export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed
-        get Document(): T {
-            return schemaCtor(this.props.Document);
-        }
-        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+        @computed get Document(): T { return schemaCtor(this.props.Document); }
+        @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document)); }
     }
     return Component;
 }
 
-
-///  DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox)
-interface DocStaticProps {
+///  DocStaticProps return a base class for React document views that have data extensions but aren't annotatable (e.g. AudioBox, FormattedTextBox)
+interface DocExtendableProps {
     Document: Doc;
     DataDoc?: Doc;
     fieldKey: string;
     isSelected: () => boolean;
     renderDepth: number;
 }
-export function DocStaticComponent<P extends DocStaticProps, T>(schemaCtor: (doc: Doc) => T) {
+export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed
-        get Document(): T {
-            return schemaCtor(this.props.Document);
-        }
-        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get Document(): T { return schemaCtor(this.props.Document); }
+        @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
         active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
     }
@@ -59,23 +51,20 @@ interface DocAnnotatableProps {
     isSelected: () => boolean;
     renderDepth: number;
 }
-export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T, fieldExt: string) {
+export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         _isChildActive = false;
-        _fieldExt = fieldExt;
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed
-        get Document(): T {
-            return schemaCtor(this.props.Document);
-        }
+        @computed get Document(): T { return schemaCtor(this.props.Document); }
+        @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
         @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-        @computed get fieldExt() { return this._fieldExt; }
+        @computed get annotationsKey() { return "annotations"; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
-            let value = this.extensionDoc && Cast(this.extensionDoc[this._fieldExt], listSpec(Doc), []);
+            let value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
             let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
             return index !== -1 && value && value.splice(index, 1) ? true : false;
         }
@@ -88,7 +77,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @action.bound
         addDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = this.props.Document;
-            return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this._fieldExt, doc) ? true : false;
+            return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
         }
 
         whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 252f90d46..b46caf3ea 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -4,27 +4,26 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { PositionDocument } from '../../new_fields/documentSchemas';
 import { List } from "../../new_fields/List";
 import { ObjectField } from '../../new_fields/ObjectField';
-import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { emptyFunction, Utils } from "../../Utils";
+import { Utils } from "../../Utils";
 import { Docs, DocUtils } from "../documents/Documents";
 import { DocumentManager } from "../util/DocumentManager";
 import { DragManager } from "../util/DragManager";
 import { SelectionManager } from "../util/SelectionManager";
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
 import { undoBatch, UndoManager } from "../util/UndoManager";
 import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
 import { CollectionView } from "./collections/CollectionView";
 import { DocumentButtonBar } from './DocumentButtonBar';
 import './DocumentDecorations.scss';
-import { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { IconBox } from "./nodes/IconBox";
 import React = require("react");
-import { TooltipTextMenu } from '../util/TooltipTextMenu';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -281,7 +280,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
 
             if (selectedDocs.length > 1) {
-                this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+                this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString(""));
                 this.moveIconDoc(this._iconDoc);
             } else {
                 this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
@@ -339,7 +338,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
 
         if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
-            const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView));
+            const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView, ""));
             iconDoc = this.createIcon([docView], layout);
         }
         return iconDoc;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d4b92a110..55a61f098 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -270,7 +270,6 @@ export class MainView extends React.Component {
                         <DocumentView Document={mainContainer}
                             DataDoc={undefined}
                             addDocument={undefined}
-                            fieldKey={"data"}
                             addDocTab={this.addDocTabFunc}
                             pinToPres={emptyFunction}
                             onClick={undefined}
@@ -360,7 +359,6 @@ export class MainView extends React.Component {
                 <DocumentView
                     Document={sidebarButtonsDoc}
                     DataDoc={undefined}
-                    fieldKey={"data"}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
                     pinToPres={emptyFunction}
@@ -387,7 +385,6 @@ export class MainView extends React.Component {
                 <DocumentView
                     Document={sidebarContent}
                     DataDoc={undefined}
-                    fieldKey={"data"}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
                     pinToPres={emptyFunction}
@@ -481,7 +478,7 @@ export class MainView extends React.Component {
                     Document={CurrentUserUtils.UserDocument.expandingButtons}
                     DataDoc={undefined}
                     fieldKey={"data"}
-                    fieldExt={""}
+                    annotationsKey={""}
                     select={emptyFunction}
                     chromeCollapsed={true}
                     active={returnFalse}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 04bc550ea..34f642f80 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -994,7 +994,6 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                     <DocumentView {...this.props}
                         DataDoc={this.props.DataDocument}
                         Document={this.props.Document}
-                        fieldKey={this.props.fieldKey}
                         fitToBox={this.props.fitToBox}
                         onClick={this.props.onClick}
                         ruleProvider={this.props.ruleProvider}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 5fb0c0f9f..7af56500e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -75,8 +75,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         this._heightMap.set(key, sectionHeight);
     }
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
-
     get Sections() {
         if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7f16fe9e0..55365de8c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -41,7 +41,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
     ruleProvider: Doc | undefined;
     children?: never | (() => JSX.Element[]) | React.ReactNode;
     isAnnotationOverlay?: boolean;
-    fieldExt: string;
+    annotationsKey: string;
 }
 
 export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@@ -74,12 +74,15 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             this._childLayoutDisposer && this._childLayoutDisposer();
         }
 
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
         // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
         // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
         // to its children which may be templates.
-        // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise.
+        // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
-            return this.props.fieldExt ? (this.extensionDoc ? this.extensionDoc[this.props.fieldExt] : undefined) : this.dataDoc[this.props.fieldKey];
+            return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs() {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index f3b0b17f8..f8eb28ade 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -32,7 +32,7 @@ library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faF
 @observer
 export class CollectionView extends React.Component<FieldViewProps> {
 
-    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
 
     private _reactionDisposer: IReactionDisposer | undefined;
     @observable private _isLightboxOpen = false;
@@ -63,18 +63,18 @@ export class CollectionView extends React.Component<FieldViewProps> {
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         let props = { ...this.props, ...renderProps };
         switch (type) {
-            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
+            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
             // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
-            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
-            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
-            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
-            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
-            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
-            case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
+            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
+            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
+            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
+            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
+            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
+            case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
             case CollectionViewType.Freeform:
             default:
                 this.props.Document.freeformLayoutEngine = undefined;
-                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
+                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
         }
         return (null);
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cc89dc2d4..0419bc3fa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -25,8 +25,8 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
-import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
 import { pageSchema } from "../../nodes/ImageBox";
 import { CollectionSubView } from "../CollectionSubView";
@@ -35,6 +35,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
 import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
+import { documentSchema, positionSchema } from "../../DocComponent";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -677,13 +678,12 @@ 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
-        return (
+        return !this.extensionDoc ? (null) :
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
                 onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
-                <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
-                    addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
-                    getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
+                <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+                    addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                         {!this.extensionDoc ? (null) :
@@ -694,8 +694,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
                 {this.overlayViews}
-            </div>
-        );
+            </div>;
     }
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3eaad3ad7..e0bf4a2f0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,23 +19,24 @@ import { CollectionViewType } from "../CollectionBaseView";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
+import { SubCollectionViewProps } from "../CollectionSubView";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
     getTransform: () => Transform;
-    container: CollectionFreeFormView;
     addDocument: (doc: Doc) => boolean;
     activeDocuments: () => Doc[];
     selectDocuments: (docs: Doc[]) => void;
     removeDocument: (doc: Doc) => boolean;
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
+    extensionDoc: Doc;
     isAnnotationOverlay?: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
 }
 
 @observer
-export class MarqueeView extends React.Component<MarqueeViewProps>
+export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps>
 {
     private _mainCont = React.createRef<HTMLDivElement>();
     @observable _lastX: number = 0;
@@ -187,13 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
 
     @action
     onPointerUp = (e: PointerEvent): void => {
-        if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+        if (!this.props.active()) this.props.selectDocuments([this.props.Document]);
         if (this._visible) {
             let mselect = this.marqueeSelect();
             if (!e.shiftKey) {
-                SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+                SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
             }
-            this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+            this.props.selectDocuments(mselect.length ? mselect : [this.props.Document]);
         }
         this.cleanupInteractions(true);
 
@@ -246,12 +247,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     }
 
     get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
-        return this.props.container.extensionDoc && Cast(this.props.container.extensionDoc.ink, InkField);
+        return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
-        let cprops = this.props.container.props;
-        this.props.container.extensionDoc && (this.props.container.extensionDoc.ink = value);
+        this.props.extensionDoc && (this.props.extensionDoc.ink = value);
     }
 
     @undoBatch
@@ -290,11 +290,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             }
             let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
                 "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
-            let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string"));
-            if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette);
-            let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
+            let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
+            if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
+            let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
             let usedPaletted = new Map<string, number>();
-            [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
+            [...this.props.activeDocuments(), this.props.Document].map(child => {
                 let bg = StrCast(Doc.Layout(child).backgroundColor);
                 if (palette.indexOf(bg) !== -1) {
                     palette.splice(palette.indexOf(bg), 1);
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 32ebe7c61..ef194624a 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -37,7 +37,7 @@ enum FollowOptions {
 @observer
 export class LinkFollowBox extends React.Component<FieldViewProps> {
 
-    public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkFollowBox, fieldKey); }
     public static Instance: LinkFollowBox | undefined;
     @observable static linkDoc: Doc | undefined = undefined;
     @observable static destinationDoc: Doc | undefined = undefined;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 3e5deb55b..4c1c3a465 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -4,18 +4,18 @@ import { observer } from "mobx-react";
 import "./AudioBox.scss";
 import { Cast } from "../../../new_fields/Types";
 import { AudioField } from "../../../new_fields/URLField";
-import { DocStaticComponent } from "../DocComponent";
+import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface } from "../../../new_fields/Schema";
-import { documentSchema } from "./DocumentView";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 type AudioDocument = makeInterface<[typeof documentSchema]>;
 const AudioDocument = makeInterface(documentSchema);
 const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
 
 @observer
-export class AudioBox extends DocStaticComponent<FieldViewProps, AudioDocument>(AudioDocument) {
+export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
 
-    public static LayoutString() { return FieldView.LayoutString(AudioBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
     _ref = React.createRef<HTMLAudioElement>();
 
     componentDidMount() {
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index b4d33fb0f..1531d825b 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -3,11 +3,11 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons';
 import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCastAsync, DocListCast } from '../../../new_fields/Doc';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
 import { List } from '../../../new_fields/List';
 import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
 import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, StrCast, Cast } from '../../../new_fields/Types';
+import { BoolCast, StrCast, Cast, FieldValue } from '../../../new_fields/Types';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { DocComponent } from '../DocComponent';
@@ -15,26 +15,28 @@ import './ButtonBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
+import { documentSchema } from '../../../new_fields/documentSchemas';
 
 
 library.add(faEdit as any);
 
 const ButtonSchema = createSchema({
     onClick: ScriptField,
+    buttonParams: listSpec("string"),
     text: "string"
 });
 
-type ButtonDocument = makeInterface<[typeof ButtonSchema]>;
-const ButtonDocument = makeInterface(ButtonSchema);
+type ButtonDocument = makeInterface<[typeof ButtonSchema, typeof documentSchema]>;
+const ButtonDocument = makeInterface(ButtonSchema, documentSchema);
 
 @observer
 export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
-    public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ButtonBox, fieldKey); }
     private dropDisposer?: DragManager.DragDropDisposer;
 
     @computed get dataDoc() {
         return this.props.DataDoc &&
-            (BoolCast(this.props.Document.isTemplateField) || BoolCast(this.props.DataDoc.isTemplateField) ||
+            (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) ||
                 this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
     }
 
@@ -52,7 +54,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
         let funcs: ContextMenuProps[] = [];
         funcs.push({
             description: "Clear Script Params", event: () => {
-                let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+                let params = FieldValue(this.Document.buttonParams);
                 params && params.map(p => this.props.Document[p] = undefined);
             }, icon: "trash"
         });
@@ -70,13 +72,13 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
     }
     // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
     render() {
-        let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+        let params = this.Document.buttonParams;
         let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
         params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... 
         return (
             <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
                 style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
-                <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
+                <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black" }} >
                     <div className="buttonBox-mainButtonCenter">
                         {(this.Document.text || this.Document.title)}
                     </div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index e3ca02fa4..58cb831f8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -2,14 +2,15 @@ import { random } from "animejs";
 import { computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { percent2frac } from "../../../Utils";
 import { Transform } from "../../util/Transform";
 import { DocComponent } from "../DocComponent";
 import "./CollectionFreeFormDocumentView.scss";
-import { documentSchema, DocumentView, DocumentViewProps } from "./DocumentView";
+import { DocumentView, DocumentViewProps } from "./DocumentView";
 import React = require("react");
+import { PositionDocument } from "../../../new_fields/documentSchemas";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -20,15 +21,6 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     jitterRotation: number;
     transition?: string;
 }
-export const positionSchema = createSchema({
-    zIndex: "number",
-    x: "number",
-    y: "number",
-    z: "number",
-});
-
-export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-export const PositionDocument = makeInterface(documentSchema, positionSchema);
 
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
@@ -92,8 +84,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     clusterColorFunc = (doc: Doc) => this.clusterColor;
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
-
     @observable _animPos: number[] | undefined = undefined;
 
     finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index fdcedb3a5..fda6d64f4 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -4,20 +4,20 @@ import { SketchPicker } from 'react-color';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ColorBox.scss";
 import { InkingControl } from "../InkingControl";
-import { DocStaticComponent } from "../DocComponent";
-import { documentSchema } from "./DocumentView";
+import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface } from "../../../new_fields/Schema";
-import { trace, reaction, observable, action, IReactionDisposer } from "mobx";
+import { reaction, observable, action, IReactionDisposer } from "mobx";
 import { SelectionManager } from "../../util/SelectionManager";
 import { StrCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
 
 @observer
-export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) {
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocument>(ColorDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
 
     _selectedDisposer: IReactionDisposer | undefined;
     _penDisposer: IReactionDisposer | undefined;
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index 22e44948a..7119b0db0 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -7,11 +7,11 @@ import { Utils } from '../../../Utils';
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragLinksAsDocuments } from "../../util/DragManager";
 import { DocComponent } from "../DocComponent";
-import { documentSchema } from "./DocumentView";
 import "./DocuLinkBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
 import React = require("react");
 import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 type DocLinkSchema = makeInterface<[typeof documentSchema]>;
 const DocLinkDocument = makeInterface(documentSchema);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9e36f0811..f741dae9a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,29 +1,37 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, runInAction, trace, observable } from "mobx";
+import { action, computed, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Document } from '../../../new_fields/documentSchemas';
 import { Id } from '../../../new_fields/FieldSymbols';
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { ImageField } from '../../../new_fields/URLField';
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, returnTrue, Utils, returnTransparent, returnOne } from "../../../Utils";
+import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils";
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from "../../documents/Documents";
+import { DocumentType } from '../../documents/DocumentTypes';
 import { ClientUtils } from '../../util/ClientUtils';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragManager, dropActionType } from "../../util/DragManager";
 import { LinkManager } from '../../util/LinkManager';
+import { Scripting } from '../../util/Scripting';
 import { SelectionManager } from "../../util/SelectionManager";
+import SharingManager from '../../util/SharingManager';
 import { Transform } from "../../util/Transform";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { CollectionViewType } from '../collections/CollectionBaseView';
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionView } from "../collections/CollectionView";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
+import { DictationOverlay } from '../DictationOverlay';
 import { DocComponent } from "../DocComponent";
 import { EditableView } from '../EditableView';
 import { OverlayView } from '../OverlayView';
@@ -33,13 +41,6 @@ import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
-import { DocumentType } from '../../documents/DocumentTypes';
-import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
-import { ImageField } from '../../../new_fields/URLField';
-import SharingManager from '../../util/SharingManager';
-import { Scripting } from '../../util/Scripting';
-import { DictationOverlay } from '../DictationOverlay';
-import { CollectionViewType } from '../collections/CollectionBaseView';
 
 library.add(fa.faEdit);
 library.add(fa.faTrash);
@@ -67,7 +68,6 @@ library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCom
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView>;
     ContainingCollectionDoc: Opt<Doc>;
-    fieldKey: string;
     Document: Doc;
     DataDoc?: Doc;
     fitToBox?: boolean;
@@ -96,41 +96,6 @@ export interface DocumentViewProps {
     layoutKey?: string;
 }
 
-export const documentSchema = createSchema({
-    // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
-    title: "string",            // document title (can be on either data document or layout)
-    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",           // "
-    backgroundColor: "string",  // background color of document
-    opacity: "number",          // opacity of document
-    //links: listSpec(Doc),       // computed (readonly) list of links associated with this document
-    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
-    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)
-    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
-    autoHeight: "boolean",      // whether the height of the document should be computed automatically based on its contents
-    isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
-    isBackground: "boolean",    // whether document is a background element and ignores input events (can only selet with marquee)
-    type: "string",             // enumerated type of document
-    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 spatially manipulated
-    inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
-    borderRounding: "string",   // border radius rounding of document
-    searchFields: "string",     // the search fields to display when this document matches a search in its metadata
-    heading: "number",          // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
-    showCaption: "string",      // whether editable caption text is overlayed at the bottom of the document 
-    showTitle: "string",        // whether an editable title banner is displayed at tht top of the document
-    isButton: "boolean",        // whether document functions as a button (overiding native interactions of its content)      
-    ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
-});
-
-
-type Document = makeInterface<[typeof documentSchema]>;
-const Document = makeInterface(documentSchema);
 
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
@@ -572,13 +537,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
     }
 
-
-    // the document containing the view layout information - will be the Document itself unless the Document has
-    // a layout field.  In that case, all layout information comes from there unless overriden by Document
-    get layoutDoc(): Document {
-        return Document(Doc.Layout(this.props.Document));
-    }
-
     // does Document set a layout prop 
     setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
     // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise.
@@ -599,7 +557,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
             Document={this.props.Document}
-            fieldKey={this.props.fieldKey}
             fitToBox={this.props.fitToBox}
             addDocument={this.props.addDocument}
             removeDocument={this.props.removeDocument}
@@ -729,6 +686,4 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 }
 
-Scripting.addGlobal(function toggleDetail(doc: any) {
-    doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout";
-});
\ No newline at end of file
+Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; });
\ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 7d69b5b51..5108954bb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -52,9 +52,8 @@ export interface FieldViewProps {
 
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
-        return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`;
-        //"<ImageBox {...props} />"
+    public static LayoutString(fieldType: { name: string }, fieldStr: string) {
+        return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`;  //e.g., "<ImageBox {...props} fieldKey={"dada} />"
     }
 
     @computed
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index fd6a475fb..ae9273639 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -17,7 +17,7 @@ type FontIconDocument = makeInterface<[typeof FontIconSchema]>;
 const FontIconDocument = makeInterface(FontIconSchema);
 @observer
 export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) {
-    public static LayoutString() { return FieldView.LayoutString(FontIconBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
     @observable _foregroundColor = "white";
     _ref: React.RefObject<HTMLButtonElement> = React.createRef();
     _backgroundReaction: IReactionDisposer | undefined;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 995fcee17..5c2d39d98 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -33,7 +33,7 @@ import { SelectionManager } from "../../util/SelectionManager";
 import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
 import { TooltipTextMenu } from "../../util/TooltipTextMenu";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocComponent } from "../DocComponent";
+import { DocExtendableComponent } from "../DocComponent";
 import { DocumentButtonBar } from '../DocumentButtonBar';
 import { DocumentDecorations } from '../DocumentDecorations';
 import { InkingControl } from "../InkingControl";
@@ -44,6 +44,7 @@ import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { documentSchema } from '../../../new_fields/documentSchemas';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -62,16 +63,14 @@ const richTextSchema = createSchema({
 
 export const GoogleRef = "googleDocId";
 
-type RichTextDocument = makeInterface<[typeof richTextSchema]>;
-const RichTextDocument = makeInterface(richTextSchema);
+type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>;
+const RichTextDocument = makeInterface(richTextSchema, documentSchema);
 
 type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
 
 @observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
-    public static LayoutString(fieldStr: string = "data") {
-        return FieldView.LayoutString(FormattedTextBox, fieldStr);
-    }
+export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
     public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
     public static Instance: FormattedTextBox;
     private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
@@ -135,17 +134,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         return true;
     }
 
-    constructor(props: FieldViewProps) {
+    constructor(props: any) {
         super(props);
         FormattedTextBox.Instance = this;
     }
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
 
-    // the document containing the view layout information - will be the Document itself unless the Document has
-    // a layout field.  In that case, all layout information comes from there unless overriden by Document
-    @computed get layoutDoc(): Doc { return Doc.Layout(this.props.Document); }
-
     linkOnDeselect: Map<string, string> = new Map();
 
     doLinkOnDeselect() {
@@ -266,8 +261,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     newLayout = Doc.MakeDelegate(draggedDoc);
                     newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
                 }
-                this.props.Document.layoutCustom = newLayout;
-                this.props.Document.layoutKey = "layoutCustom";
+                this.Document.layoutCustom = newLayout;
+                this.Document.layoutKey = "layoutCustom";
                 e.stopPropagation();
                 // embed document when dragging with a userDropAction or an embedDoc flag set
             } else if (de.data.userDropAction || de.data.embedDoc) {
@@ -1015,7 +1010,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
         if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
-            let nh = this.props.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
+            let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
             let dh = NumCast(this.layoutDoc.height, 0);
             this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
             this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
@@ -1052,7 +1047,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 onPointerEnter={action(() => this._entered = true)}
                 onPointerLeave={action(() => this._entered = false)}
             >
-                <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.props.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
+                <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-dictation"
                     onClick={e => {
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 4971f61b7..60f547b1e 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -24,7 +24,7 @@ library.add(faFilm, faTag, faTextHeight);
 
 @observer
 export class IconBox extends React.Component<FieldViewProps> {
-    public static LayoutString() { return FieldView.LayoutString(IconBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(IconBox, fieldKey); }
 
     @observable _panelWidth: number = 0;
     @observable _panelHeight: number = 0;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4d623a04f..4c2f8e22d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -20,12 +20,12 @@ import { ContextMenu } from "../../views/ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocAnnotatableComponent } from '../DocComponent';
 import { InkingControl } from '../InkingControl';
-import { documentSchema } from './DocumentView';
 import FaceRectangles from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ImageBox.scss";
 import React = require("react");
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { documentSchema } from '../../../new_fields/documentSchemas';
 var requestImageSize = require('../../util/request-image-size');
 var path = require('path');
 const { Howl } = require('howler');
@@ -38,7 +38,9 @@ library.add(faFileAudio, faAsterisk);
 export const pageSchema = createSchema({
     curPage: "number",
     fitWidth: "boolean",
-    rotation: "number"
+    rotation: "number",
+    googlePhotosUrl: "string",
+    googlePhotosTags: "string"
 });
 
 interface Window {
@@ -54,8 +56,8 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
 const ImageDocument = makeInterface(pageSchema, documentSchema);
 
 @observer
-export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument, "annotations") {
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(ImageBox, fieldKey); }
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
     private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _audioState = 0;
@@ -251,7 +253,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     audioDown = () => this.recordAudioAnnotation();
 
     considerGooglePhotosLink = () => {
-        const remoteUrl = StrCast(this.props.Document.googlePhotosUrl);
+        const remoteUrl = this.Document.googlePhotosUrl;
         return !remoteUrl ? (null) : (<img
             id={"google-photos"}
             src={"/assets/google_photos.png"}
@@ -260,7 +262,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     considerGooglePhotosTags = () => {
-        const tags = StrCast(this.props.Document.googlePhotosTags);
+        const tags = this.Document.googlePhotosTags;
         return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
     }
 
@@ -287,7 +289,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         if (field instanceof ImageField) paths = [this.choosePath(field.url)];
         paths.push(...altpaths);
         // }
-        let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
+        let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive";
         let rotation = NumCast(this.Document.rotation, 0);
         let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
         let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
@@ -333,7 +335,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={this.fieldExt}
+                annotationsKey={this.annotationsKey}
                 isAnnotationOverlay={true}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2b46e9dff..35e9e4862 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -25,11 +25,12 @@ export type KVPScript = {
 
 @observer
 export class KeyValueBox extends React.Component<FieldViewProps> {
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+
     private _mainCont = React.createRef<HTMLDivElement>();
     private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
-    @observable private rows: KeyValuePair[] = [];
 
-    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+    @observable private rows: KeyValuePair[] = [];
     @observable private _keyInput: string = "";
     @observable private _valueInput: string = "";
     @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b1110daf4..396a5356a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -16,18 +16,18 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocAnnotatableComponent } from "../DocComponent";
 import { PDFViewer } from "../pdf/PDFViewer";
-import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
+import { documentSchema } from '../../../new_fields/documentSchemas';
 
 type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
 
 @observer
-export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument, "annotations") {
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(PDFBox, fieldKey); }
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
     private _keyValue: string = "";
     private _valueValue: string = "";
     private _scriptValue: string = "";
@@ -232,7 +232,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
                     pinToPres={this.props.pinToPres} addDocument={this.addDocument}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                     isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
-                    fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
+                    fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
                 {this.settingsPanel()}
             </div>);
     }
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 15fafb022..7ec5d0471 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -31,7 +31,7 @@ library.add(faEdit);
 
 @observer
 export class PresBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
     _docListChangedReaction: IReactionDisposer | undefined;
     componentDidMount() {
         this._docListChangedReaction = reaction(() => {
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
index ced597b59..99b5810fc 100644
--- a/src/client/views/nodes/QueryBox.tsx
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -18,7 +18,7 @@ library.add(faEdit);
 
 @observer
 export class QueryBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
     _docListChangedReaction: IReactionDisposer | undefined;
     componentDidMount() {
     }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 64871ef41..48a699e58 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,33 +1,32 @@
 import React = require("react");
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx";
+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, reaction, runInAction, untracked } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from 'request-promise';
+import { Doc } from "../../../new_fields/Doc";
 import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface, createSchema, listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
+import { createSchema, 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 { RouteStore } from "../../../server/RouteStore";
-import { Utils, emptyFunction, returnOne } from "../../../Utils";
+import { emptyFunction, 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 { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
-import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faVideo } from "@fortawesome/free-solid-svg-icons";
-import { Doc } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { positionSchema } from "./CollectionFreeFormDocumentView";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
 var path = require('path');
 
 export const timeSchema = createSchema({
-    currentTimecode: "number",
+    currentTimecode: "number",  // the current time of a video or other linear, time-based document.  Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
 });
 type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>;
 const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
@@ -35,7 +34,7 @@ const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
 library.add(faVideo);
 
 @observer
-export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument, "annotations") {
+export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
     static _youtubeIframeCounter: number = 0;
     private _reactionDisposer?: IReactionDisposer;
     private _youtubeReactionDisposer?: IReactionDisposer;
@@ -49,7 +48,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     @observable _fullScreen = false;
     @observable _playing = false;
     @observable static _showControls: boolean;
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(VideoBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
 
     public get player(): HTMLVideoElement | null {
         return this._videoRef;
@@ -282,24 +281,20 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         ]]);
     }
 
-    @action
-    onPlayDown = () => this._playing ? this.Pause() : this.Play()
+    onPlayDown = () => this._playing ? this.Pause() : this.Play();
 
-    @action
     onFullDown = (e: React.PointerEvent) => {
         this.FullScreen();
         e.stopPropagation();
         e.preventDefault();
     }
 
-    @action
     onSnapshot = (e: React.PointerEvent) => {
         this.Snapshot();
         e.stopPropagation();
         e.preventDefault();
     }
 
-    @action
     onResetDown = (e: React.PointerEvent) => {
         this.Pause();
         e.stopPropagation();
@@ -309,12 +304,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         InkingControl.Instance.switchTool(InkTool.Eraser);
     }
 
-    @action
     onResetMove = (e: PointerEvent) => {
         this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
         this.Seek(Math.max(0, (this.Document.currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
         e.stopImmediatePropagation();
     }
+
     @action
     onResetUp = (e: PointerEvent) => {
         document.removeEventListener("pointermove", this.onResetMove, true);
@@ -322,7 +317,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         InkingControl.Instance.switchTool(InkTool.None);
         this._isResetClick < 10 && (this.Document.currentTimecode = 0);
     }
-    @computed get dataDoc() { return this.props.DataDoc && this.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
@@ -346,7 +340,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={this.fieldExt}
+                annotationsKey={this.annotationsKey}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 2184325a9..5af743859 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -19,8 +19,8 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from "./KeyValueBox";
 import "./WebBox.scss";
 import React = require("react");
-import { documentSchema } from "./DocumentView";
 import { DocAnnotatableComponent } from "../DocComponent";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 library.add(faStickyNote);
 
@@ -28,13 +28,12 @@ type WebDocument = makeInterface<[typeof documentSchema]>;
 const WebDocument = makeInterface(documentSchema);
 
 @observer
-export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument, "annotations") {
+export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
 
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(WebBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
     @observable private collapsed: boolean = true;
     @observable private url: string = "";
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
     componentWillMount() {
 
         let field = Cast(this.props.Document[this.props.fieldKey], WebField);
@@ -199,7 +198,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={this.fieldExt}
+                annotationsKey={this.annotationsKey}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5806dec83..51dc6e8d6 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -24,8 +24,8 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec
 import { SelectionManager } from "../../util/SelectionManager";
 import { undoBatch } from "../../util/UndoManager";
 import { DocAnnotatableComponent } from "../DocComponent";
-import { documentSchema } from "../nodes/DocumentView";
 import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -49,7 +49,6 @@ interface IViewerProps {
     Document: Doc;
     DataDoc?: Doc;
     ContainingCollectionView: Opt<CollectionView>;
-    extensionDoc: Doc;
     PanelWidth: () => number;
     PanelHeight: () => number;
     ContentScaling: () => number;
@@ -73,7 +72,7 @@ interface IViewerProps {
  * Handles rendering and virtualization of the pdf
  */
 @observer
-export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument, "annotations") {
+export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) {
     static _annotationStyle: any = addStyleSheet();
     @observable private _pageSizes: { width: number, height: number }[] = [];
     @observable private _annotations: Doc[] = [];
@@ -109,8 +108,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     private _coverPath: any;
 
     @computed get allAnnotations() {
-        return DocListCast(this.props.extensionDoc.annotations).filter(
-            anno => this._script.run({ this: anno }, console.log, true).result);
+        return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter(
+            anno => this._script.run({ this: anno }, console.log, true).result) : [];
     }
 
     @computed get nonDocAnnotations() {
@@ -200,7 +199,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         await this.initialLoad();
 
         this._annotationReactionDisposer = reaction(
-            () => this.props.extensionDoc && DocListCast(this.props.extensionDoc.annotations),
+            () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
             annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
             { fireImmediately: true });
 
@@ -629,10 +628,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @computed get annotationLayer() {
         return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0) }} ref={this._annotationLayer}>
             {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
-                <Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)}
+                <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
             <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
                 <CollectionFreeFormView {...this.props}
-                    fieldExt={this.fieldExt}
+                    annotationsKey={this.annotationsKey}
                     setPreviewCursor={this.setPreviewCursor}
                     PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)}
                     PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)}
@@ -673,7 +672,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     marqueeing = () => this._marqueeing;
     visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
     render() {
-        return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")}
+        return !this.extensionDoc ? (null) : (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")}
             onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
             {this.pdfViewerDiv}
             {this.annotationLayer}
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 0824808eb..5f758f496 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -5,18 +5,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { documentSchema } from '../../../new_fields/documentSchemas';
 import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils";
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse } from "../../../Utils";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { Transform } from "../../util/Transform";
 import { CollectionViewType } from '../collections/CollectionBaseView';
-import { DocumentView } from "../nodes/DocumentView";
+import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
+import { DocComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import "./PresElementBox.scss";
 import React = require("react");
-import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
-
 
 library.add(faArrowUp);
 library.add(fileSolid);
@@ -24,34 +25,34 @@ library.add(faLocationArrow);
 library.add(fileRegular as any);
 library.add(faSearch);
 library.add(faArrowDown);
+
+export const presSchema = createSchema({
+    presentationTargetDoc: Doc,
+    presBox: Doc,
+    presBoxKey: "string",
+    showButton: "boolean",
+    navButton: "boolean",
+    hideTillShownButton: "boolean",
+    fadeButton: "boolean",
+    hideAfterButton: "boolean",
+    groupButton: "boolean",
+    embedOpen: "boolean"
+});
+
+type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
+const PresDocument = makeInterface(presSchema, documentSchema);
 /**
  * This class models the view a document added to presentation will have in the presentation.
  * It involves some functionality for its buttons and options.
  */
 @observer
-export class PresElementBox extends React.Component<FieldViewProps> {
+export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(PresDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
 
-    public static LayoutString() { return FieldView.LayoutString(PresElementBox); }
-
-    @computed get myIndex() { return DocListCast(this.presentationDoc[this.presentationFieldKey]).indexOf(this.props.Document); }
-    @computed get presentationDoc() { return this.props.Document.presBox as Doc; }
-    @computed get presentationFieldKey() { return StrCast(this.props.Document.presBoxKey); }
+    @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); }
-    @computed get showButton() { return BoolCast(this.props.Document.showButton); }
-    @computed get navButton() { return BoolCast(this.props.Document.navButton); }
-    @computed get hideTillShownButton() { return BoolCast(this.props.Document.hideTillShownButton); }
-    @computed get fadeButton() { return BoolCast(this.props.Document.fadeButton); }
-    @computed get hideAfterButton() { return BoolCast(this.props.Document.hideAfterButton); }
-    @computed get groupButton() { return BoolCast(this.props.Document.groupButton); }
-    @computed get embedOpen() { return BoolCast(this.props.Document.embedOpen); }
-
-    set embedOpen(value: boolean) { this.props.Document.embedOpen = value; }
-    set showButton(val: boolean) { this.props.Document.showButton = val; }
-    set navButton(val: boolean) { this.props.Document.navButton = val; }
-    set hideTillShownButton(val: boolean) { this.props.Document.hideTillShownButton = val; }
-    set fadeButton(val: boolean) { this.props.Document.fadeButton = val; }
-    set hideAfterButton(val: boolean) { this.props.Document.hideAfterButton = val; }
-    set groupButton(val: boolean) { this.props.Document.groupButton = val; }
 
     /**
      * The function that is called on click to turn Hiding document till press option on/off.
@@ -60,14 +61,14 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.hideTillShownButton = !this.hideTillShownButton;
-        if (!this.hideTillShownButton) {
-            if (this.myIndex >= this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+        this.Document.hideTillShownButton = !this.Document.hideTillShownButton;
+        if (!this.Document.hideTillShownButton) {
+            if (this.indexInPres >= this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 1;
             }
         } else {
-            if (this.presentationDoc.presStatus && this.myIndex > this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 0;
+            if (this.presentationDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 0;
             }
         }
     }
@@ -80,15 +81,15 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.hideAfterButton = !this.hideAfterButton;
-        if (!this.hideAfterButton) {
-            if (this.myIndex <= this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+        this.Document.hideAfterButton = !this.Document.hideAfterButton;
+        if (!this.Document.hideAfterButton) {
+            if (this.indexInPres <= this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 1;
             }
         } else {
-            if (this.fadeButton) this.fadeButton = false;
-            if (this.presentationDoc.presStatus && this.myIndex < this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 0;
+            if (this.Document.fadeButton) this.Document.fadeButton = false;
+            if (this.presentationDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 0;
             }
         }
     }
@@ -101,15 +102,15 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.fadeButton = !this.fadeButton;
-        if (!this.fadeButton) {
-            if (this.myIndex <= this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+        this.Document.fadeButton = !this.Document.fadeButton;
+        if (!this.Document.fadeButton) {
+            if (this.indexInPres <= this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 1;
             }
         } else {
-            this.hideAfterButton = false;
-            if (this.presentationDoc.presStatus && (this.myIndex < this.currentIndex)) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 0.5;
+            this.Document.hideAfterButton = false;
+            if (this.presentationDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
+                this.targetDoc.opacity = 0.5;
             }
         }
     }
@@ -120,10 +121,10 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onNavigateDocumentClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.navButton = !this.navButton;
-        if (this.navButton) {
-            this.showButton = false;
-            if (this.currentIndex === this.myIndex) {
+        this.Document.navButton = !this.Document.navButton;
+        if (this.Document.navButton) {
+            this.Document.showButton = false;
+            if (this.currentIndex === this.indexInPres) {
                 this.props.focus(this.props.Document);
             }
         }
@@ -136,12 +137,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     onZoomDocumentClick = (e: React.MouseEvent) => {
         e.stopPropagation();
 
-        this.showButton = !this.showButton;
-        if (!this.showButton) {
+        this.Document.showButton = !this.Document.showButton;
+        if (!this.Document.showButton) {
             this.props.Document.viewScale = 1;
         } else {
-            this.navButton = false;
-            if (this.currentIndex === this.myIndex) {
+            this.Document.navButton = false;
+            if (this.currentIndex === this.indexInPres) {
                 this.props.focus(this.props.Document);
             }
         }
@@ -151,13 +152,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
      */
     ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
     /**
      * The function that is responsible for rendering the a preview or not for this
      * presentation element.
      */
     renderEmbeddedInline = () => {
-        if (!this.embedOpen || !(this.props.Document.presentationTargetDoc instanceof Doc)) {
+        if (!this.Document.embedOpen || !this.targetDoc) {
             return (null);
         }
 
@@ -170,8 +170,9 @@ export class PresElementBox extends React.Component<FieldViewProps> {
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <CollectionSchemaPreview
-                    fitToBox={StrCast(this.props.Document.presentationTargetDoc.type).indexOf(DocumentType.COL) !== -1}
-                    Document={this.props.Document.presentationTargetDoc}
+                    fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
+                    Document={this.targetDoc}
+                    fieldKey={this.props.fieldKey}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
                     ruleProvider={undefined}
@@ -193,30 +194,27 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     }
 
     render() {
-        let p = this.props;
-
         let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
-        let className = "presElementBox-item" + (this.currentIndex === this.myIndex ? " presElementBox-selected" : "");
+        let className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
         let pbi = "presElementBox-interaction";
         return (
-            <div className={className} key={p.Document[Id] + this.myIndex}
-                style={{ outlineWidth: Doc.IsBrushed(p.Document.presentationTargetDoc as Doc) ? `1px` : "0px", }}
-                onClick={e => { p.focus(p.Document); e.stopPropagation(); }}>
+            <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(); }}>
                 {treecontainer ? (null) : <>
                     <strong className="presElementBox-name">
-                        {`${this.myIndex + 1}. ${p.Document.title}`}
+                        {`${this.indexInPres + 1}. ${this.Document.title}`}
                     </strong>
-                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button>
+                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.props.Document)}>X</button>
                     <br />
-                </>
-                }
-                <button title="Zoom" className={pbi + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
-                <button title="Navigate" className={pbi + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
-                <button title="Hide Before" className={pbi + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
-                <button title="Fade After" className={pbi + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Hide After" className={pbi + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Group With Up" className={pbi + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}><FontAwesomeIcon icon={"arrow-up"} /></button>
-                <button title="Expand Inline" className={pbi + (this.embedOpen ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.embedOpen = !this.embedOpen; })}><FontAwesomeIcon icon={"arrow-down"} /></button>
+                </>}
+                <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>
 
                 <br style={{ lineHeight: 0.1 }} />
                 {this.renderEmbeddedInline()}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index c6f654c33..08cb66d5f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -645,7 +645,8 @@ export namespace Doc {
         @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
     }
 
-    // returns the active layout document for 'doc'.  
+    // the document containing the view layout information - will be the Document itself unless the Document has
+    // a layout field.  In that case, all layout information comes from there unless overriden by Document  
     export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? doc[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
     export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
     const manager = new DocData();
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
new file mode 100644
index 000000000..8c3b62067
--- /dev/null
+++ b/src/new_fields/documentSchemas.ts
@@ -0,0 +1,51 @@
+import { makeInterface, createSchema, listSpec } from "./Schema";
+import { ScriptField } from "./ScriptField";
+import { Doc } from "./Doc";
+
+export const documentSchema = createSchema({
+    // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
+    layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
+    layoutCustom: 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)
+    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",           // "
+    color: "string",            // foreground color of document
+    backgroundColor: "string",  // background color of document
+    opacity: "number",          // opacity of document
+    //links: listSpec(Doc),       // computed (readonly) list of links associated with this document
+    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+    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)
+    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
+    autoHeight: "boolean",      // whether the height of the document should be computed automatically based on its contents
+    isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
+    isBackground: "boolean",    // whether document is a background element and ignores input events (can only selet with marquee)
+    type: "string",             // enumerated type of document
+    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 spatially manipulated
+    inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
+    borderRounding: "string",   // border radius rounding of document
+    searchFields: "string",     // the search fields to display when this document matches a search in its metadata
+    heading: "number",          // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
+    showCaption: "string",      // whether editable caption text is overlayed at the bottom of the document 
+    showTitle: "string",        // whether an editable title banner is displayed at tht top of the document
+    isButton: "boolean",        // whether document functions as a button (overiding native interactions of its content)      
+    ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
+});
+
+export const positionSchema = createSchema({
+    zIndex: "number",
+    x: "number",
+    y: "number",
+    z: "number",
+});
+
+export type Document = makeInterface<[typeof documentSchema]>;
+export const Document = makeInterface(documentSchema);
+
+export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+export const PositionDocument = makeInterface(documentSchema, positionSchema);
-- 
cgit v1.2.3-70-g09d2


From 1089aa451fd4571545e06b5674bc02ed1ecf4361 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 22 Oct 2019 11:32:43 -0400
Subject: added recording to audio documents and added to toolbar

---
 src/client/views/MainView.tsx                      |   3 +-
 src/client/views/nodes/AudioBox.scss               |  14 ++-
 src/client/views/nodes/AudioBox.tsx                | 137 +++++++++++++++++++--
 src/client/views/pdf/Annotation.tsx                |   1 +
 .../authentication/models/current_user_utils.ts    |   1 +
 5 files changed, 142 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 26e70c5c7..8bad70093 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import {
     faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
-    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter
+    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -108,6 +108,7 @@ export class MainView extends React.Component {
         library.add(faEraser);
         library.add(faFileAudio);
         library.add(faPenNib);
+        library.add(faMicrophone);
         library.add(faFilm);
         library.add(faMusic);
         library.add(faTree);
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 04d98e10d..5c43c3c00 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,8 +1,9 @@
-.audiobox-container {
+.audiobox-container, .audiobox-container-interactive {
     width: 100%;
     height: 100%;
     position: inherit;
     display:inline-block;
+    pointer-events: all;
     .audiobox-control, .audiobox-control-interactive {
         top:0;
         max-height: 32px;
@@ -13,4 +14,15 @@
     .audiobox-control-interactive {
         pointer-events: all;
     }
+    .audiobox-record {
+        pointer-events: all;
+        width:100%;
+        height:100%;
+        position: absolute;
+        pointer-events: none;
+    }
+    .audiobox-record-interactive {
+        pointer-events: all;
+
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 4c1c3a465..ee4e06a2e 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,37 +2,150 @@ import React = require("react");
 import { FieldViewProps, FieldView } from './FieldView';
 import { observer } from "mobx-react";
 import "./AudioBox.scss";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, DateCast } from "../../../new_fields/Types";
 import { AudioField } from "../../../new_fields/URLField";
 import { DocExtendableComponent } from "../DocComponent";
-import { makeInterface } from "../../../new_fields/Schema";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
 import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Utils } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { runInAction, observable, reaction, IReactionDisposer, computed } from "mobx";
+import { DateField } from "../../../new_fields/DateField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Doc } from "../../../new_fields/Doc";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
 
-type AudioDocument = makeInterface<[typeof documentSchema]>;
-const AudioDocument = makeInterface(documentSchema);
+interface Window {
+    MediaRecorder: MediaRecorder;
+}
+
+declare class MediaRecorder {
+    // whatever MediaRecorder has
+    constructor(e: any);
+}
+export const audioSchema = createSchema({
+    playOnSelect: "boolean"
+});
+
+type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
+const AudioDocument = makeInterface(documentSchema, audioSchema);
 const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
 
 @observer
 export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
 
+    _reactionDisposer: IReactionDisposer | undefined;
+    @observable private _audioState = 0;
+
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
     _ref = React.createRef<HTMLAudioElement>();
 
     componentDidMount() {
-        if (this._ref.current) this._ref.current.currentTime = 1;
+        runInAction(() => this._audioState = this.path ? 2 : 0);
+        this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+            selected => {
+                let sel = selected.length ? selected[0].props.Document : undefined;
+                const extensionDoc = this.extensionDoc;
+                let start = extensionDoc && DateCast(extensionDoc.recordingStart);
+                let seek = sel && DateCast(sel.creationDate)
+                if (this._ref.current && start && seek) {
+                    if (this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document)) {
+                        let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
+                        if (start && seek && delta > 0 && delta < this._ref.current.duration) {
+                            this._ref.current.currentTime = delta;
+                            this._ref.current.play();
+                        } else {
+                            this._ref.current.pause();
+                        }
+                    } else {
+                        this._ref.current.pause();
+                    }
+                }
+            });
     }
 
-    render() {
+    componentWillUnmount() {
+        this._reactionDisposer && this._reactionDisposer();
+    }
+
+    _recorder: any;
+    recordAudioAnnotation = () => {
+        let gumStream: any;
+        let self = this;
+        const extensionDoc = this.extensionDoc;
+        extensionDoc && navigator.mediaDevices.getUserMedia({
+            audio: true
+        }).then(function (stream) {
+            gumStream = stream;
+            self._recorder = new MediaRecorder(stream);
+            extensionDoc.recordingStart = new DateField(new Date());
+            self._recorder.ondataavailable = async function (e: any) {
+                const formData = new FormData();
+                formData.append("file", e.data);
+                const res = await fetch(Utils.prepend(RouteStore.upload), {
+                    method: 'POST',
+                    body: formData
+                });
+                const files = await res.json();
+                const url = Utils.prepend(files[0].path);
+                // upload to server with known URL 
+                self.props.Document[self.props.fieldKey] = new AudioField(url);
+            };
+            runInAction(() => self._audioState = 1);
+            self._recorder.start();
+            setTimeout(() => {
+                self._recorder.stop();
+                runInAction(() => self._audioState = 2);
+                gumStream.getAudioTracks()[0].stop();
+            }, 60 * 60 * 1000); // stop after an hour?
+        });
+    }
+
+    specificContextMenu = (e: React.MouseEvent): void => {
+        let funcs: ContextMenuProps[] = [];
+        funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" });
+
+        ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
+    }
+
+    recordClick = (e: React.MouseEvent) => {
+        if (e.button === 0) {
+            if (this._recorder) {
+                this._recorder.stop();
+                runInAction(() => this._audioState = 2);
+            } else {
+                this.recordAudioAnnotation();
+            }
+            e.stopPropagation();
+        }
+    }
+
+    @computed get path() {
         let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
         let path = field.url.href;
+        return path === "https://actions.google.com/sounds/v1/alarms/beep_short.ogg" ? "" : path;
+    }
+
+    @computed get audio() {
+        let interactive = this.active() ? "-interactive" : "";
+        return <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
+            <source src={this.path} type="audio/mpeg" />
+            Not supported.
+        </audio>;
+    }
+
+    render() {
 
         let interactive = this.active() ? "-interactive" : "";
-        return (
-            <div className="audiobox-container">
-                <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
-                    <source src={path} type="audio/mpeg" />
-                    Not supported.
-                </audio>
+        return (!this.extensionDoc ? (null) :
+            <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
+                {!this.path ?
+                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}>
+                        {this._audioState === 1 ? "STOP" : "RECORD"}
+                    </button> :
+                    this.audio
+                }
             </div>
         );
     }
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index e0a3b9171..2d8f47666 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -102,6 +102,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
                 DocumentManager.Instance.FollowLink(undefined, annoGroup,
                     (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"),
                     false, false, undefined);
+                e.stopPropagation();
             }
         }
     }
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 532f527cc..cf4ae4e6c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -50,6 +50,7 @@ export class CurrentUserUtils {
             { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
             { 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: "record", icon: "microphone", ignoreClick: true, drag: 'Docs.Create.AudioDocument("https://actions.google.com/sounds/v1/alarms/beep_short.ogg", { 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", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
             { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
-- 
cgit v1.2.3-70-g09d2


From 79476255d05e0bd7c24e46267507769800c547f8 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 22 Oct 2019 11:44:08 -0400
Subject: from last

---
 src/client/views/nodes/AudioBox.tsx                    | 11 ++++-------
 src/new_fields/URLField.ts                             |  2 ++
 src/server/authentication/models/current_user_utils.ts |  3 ++-
 3 files changed, 8 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index ee4e06a2e..5f5202f65 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -3,7 +3,7 @@ import { FieldViewProps, FieldView } from './FieldView';
 import { observer } from "mobx-react";
 import "./AudioBox.scss";
 import { Cast, DateCast } from "../../../new_fields/Types";
-import { AudioField } from "../../../new_fields/URLField";
+import { AudioField, nullAudio } from "../../../new_fields/URLField";
 import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface, createSchema } from "../../../new_fields/Schema";
 import { documentSchema } from "../../../new_fields/documentSchemas";
@@ -30,11 +30,9 @@ export const audioSchema = createSchema({
 
 type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
 const AudioDocument = makeInterface(documentSchema, audioSchema);
-const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
 
 @observer
 export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
-
     _reactionDisposer: IReactionDisposer | undefined;
     @observable private _audioState = 0;
 
@@ -122,9 +120,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     @computed get path() {
-        let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
-        let path = field.url.href;
-        return path === "https://actions.google.com/sounds/v1/alarms/beep_short.ogg" ? "" : path;
+        let field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+        let path = (field instanceof AudioField) ? field.url.href : "";
+        return path === nullAudio ? "" : path;
     }
 
     @computed get audio() {
@@ -136,7 +134,6 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     render() {
-
         let interactive = this.active() ? "-interactive" : "";
         return (!this.extensionDoc ? (null) :
             <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index b9ad96450..35ef6dd02 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -38,6 +38,8 @@ export abstract class URLField extends ObjectField {
     }
 }
 
+export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg";
+
 @scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
 @scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
 @scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index cf4ae4e6c..7897a464c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -14,6 +14,7 @@ import { Utils } from "../../../Utils";
 import { RouteStore } from "../../RouteStore";
 import { InkingControl } from "../../../client/views/InkingControl";
 import { DragManager } from "../../../client/util/DragManager";
+import { nullAudio } from "../../../new_fields/URLField";
 
 export class CurrentUserUtils {
     private static curr_id: string;
@@ -50,7 +51,7 @@ export class CurrentUserUtils {
             { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
             { 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: "record", icon: "microphone", ignoreClick: true, drag: 'Docs.Create.AudioDocument("https://actions.google.com/sounds/v1/alarms/beep_short.ogg", { width: 200, title: "ready to record audio" })' },
+            { 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", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
             { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
-- 
cgit v1.2.3-70-g09d2


From 0206517bbf09c10b8f3926c7ea133fa4c0263a3a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 22 Oct 2019 14:20:16 -0400
Subject: using links to trigger audio playback.

---
 src/client/documents/Documents.ts                  |  4 ++
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/AudioBox.tsx                | 80 ++++++++++++++--------
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 4 files changed, 59 insertions(+), 29 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b1406d5e1..fd2009dd6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -334,6 +334,10 @@ export namespace Docs {
             let dataDoc = MakeDataDelegate(proto, protoProps, data);
             let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
 
+            AudioBox.ActiveRecordings.map(d => {
+                DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title);
+            })
+
             return Doc.assign(viewDoc, delegateProps);
         }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d7350fe1a..f6518a01d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -263,7 +263,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.nativeEvent.cancelBubble) return;
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
-        if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
+        if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
             document.removeEventListener("pointermove", this.onPointerMove);
             document.removeEventListener("pointerup", this.onPointerUp);
             document.addEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 5f5202f65..85607c6b8 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -9,12 +9,13 @@ import { makeInterface, createSchema } from "../../../new_fields/Schema";
 import { documentSchema } from "../../../new_fields/documentSchemas";
 import { Utils } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
-import { runInAction, observable, reaction, IReactionDisposer, computed } from "mobx";
+import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
 import { DateField } from "../../../new_fields/DateField";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ContextMenu } from "../ContextMenu";
+import { Id } from "../../../new_fields/FieldSymbols";
 
 interface Window {
     MediaRecorder: MediaRecorder;
@@ -33,41 +34,60 @@ const AudioDocument = makeInterface(documentSchema, audioSchema);
 
 @observer
 export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
-    _reactionDisposer: IReactionDisposer | undefined;
-    @observable private _audioState = 0;
-
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
+
+    _linkPlayDisposer: IReactionDisposer | undefined;
+    _reactionDisposer: IReactionDisposer | undefined;
     _ref = React.createRef<HTMLAudioElement>();
+    _recorder: any;
+
+    @observable private _audioState = 0;
+    public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
         runInAction(() => this._audioState = this.path ? 2 : 0);
+        this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
+            scrollLinkId => {
+                scrollLinkId && DocListCast(this.dataDoc.links).map(l => {
+                    const la1 = l.anchor1 as Doc;
+                    const la2 = l.anchor2 as Doc;
+                    if (l[Id] === scrollLinkId && la1 && la2) {
+                        setTimeout(() => this.playFrom(Doc.AreProtosEqual(la1, this.dataDoc) ? la2 : la1), 250);
+                    }
+                });
+                scrollLinkId && (this.layoutDoc.scrollLinkID = undefined);
+            }, { fireImmediately: true });
         this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             selected => {
                 let sel = selected.length ? selected[0].props.Document : undefined;
-                const extensionDoc = this.extensionDoc;
-                let start = extensionDoc && DateCast(extensionDoc.recordingStart);
-                let seek = sel && DateCast(sel.creationDate)
-                if (this._ref.current && start && seek) {
-                    if (this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document)) {
-                        let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
-                        if (start && seek && delta > 0 && delta < this._ref.current.duration) {
-                            this._ref.current.currentTime = delta;
-                            this._ref.current.play();
-                        } else {
-                            this._ref.current.pause();
-                        }
-                    } else {
-                        this._ref.current.pause();
-                    }
-                }
+                this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(sel);
             });
     }
 
+    playFrom = (sel: Doc) => {
+        const extensionDoc = this.extensionDoc;
+        let start = extensionDoc && DateCast(extensionDoc.recordingStart);
+        let seek = sel && DateCast(sel.creationDate)
+        if (this._ref.current && start && seek) {
+            if (sel) {
+                let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
+                if (start && seek && delta > 0 && delta < this._ref.current.duration) {
+                    this._ref.current.currentTime = delta;
+                    this._ref.current.play();
+                } else {
+                    this._ref.current.pause();
+                }
+            } else {
+                this._ref.current.pause();
+            }
+        }
+    }
+
     componentWillUnmount() {
         this._reactionDisposer && this._reactionDisposer();
+        this._linkPlayDisposer && this._linkPlayDisposer();
     }
 
-    _recorder: any;
     recordAudioAnnotation = () => {
         let gumStream: any;
         let self = this;
@@ -78,6 +98,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             gumStream = stream;
             self._recorder = new MediaRecorder(stream);
             extensionDoc.recordingStart = new DateField(new Date());
+            AudioBox.ActiveRecordings.push(self.props.Document);
             self._recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
@@ -93,8 +114,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             runInAction(() => self._audioState = 1);
             self._recorder.start();
             setTimeout(() => {
-                self._recorder.stop();
-                runInAction(() => self._audioState = 2);
+                self.stopRecording();
                 gumStream.getAudioTracks()[0].stop();
             }, 60 * 60 * 1000); // stop after an hour?
         });
@@ -107,11 +127,17 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
     }
 
+    stopRecording = action(() => {
+        this._recorder.stop();
+        this._audioState = 2;
+        let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
+        ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
+    })
+
     recordClick = (e: React.MouseEvent) => {
-        if (e.button === 0) {
+        if (e.button === 0 && !e.ctrlKey) {
             if (this._recorder) {
-                this._recorder.stop();
-                runInAction(() => this._audioState = 2);
+                this.stopRecording();
             } else {
                 this.recordAudioAnnotation();
             }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 05080b824..67c85e158 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -218,7 +218,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 this._hitTemplateDrag = true;
             }
         }
-        if ((this.active || this.Document.onDragStart || this.Document.onClick) && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active.  if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
+        if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active.  if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
         document.removeEventListener("pointermove", this.onPointerMove);
         document.removeEventListener("pointerup", this.onPointerUp);
         document.addEventListener("pointermove", this.onPointerMove);
-- 
cgit v1.2.3-70-g09d2


From 171e5a1716e4a2f981e647ae26f8ddd0e2332693 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 22 Oct 2019 19:15:29 -0400
Subject: playback highlights from audio

---
 src/client/documents/Documents.ts    |  2 +-
 src/client/views/MainView.tsx        |  2 +-
 src/client/views/nodes/AudioBox.scss | 12 ++++++--
 src/client/views/nodes/AudioBox.tsx  | 58 ++++++++++++++++++++++++++----------
 4 files changed, 54 insertions(+), 20 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fd2009dd6..316efe44c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -336,7 +336,7 @@ export namespace Docs {
 
             AudioBox.ActiveRecordings.map(d => {
                 DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title);
-            })
+            });
 
             return Doc.assign(viewDoc, delegateProps);
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c14739aa2..78b8ac0b7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -449,7 +449,7 @@ export class MainView extends React.Component {
     public static expandFlyout = action(() => {
         MainView.Instance._flyoutTranslate = true;
         MainView.Instance.flyoutWidth = 250;
-    })
+    });
 
     @computed get expandButton() {
         return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null);
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 5c43c3c00..9bda5b2a7 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,14 +1,21 @@
 .audiobox-container, .audiobox-container-interactive {
     width: 100%;
     height: 100%;
+    min-height: 32px;
     position: inherit;
-    display:inline-block;
+    display:flex;
     pointer-events: all;
+    .audiobox-handle {
+        width:20px;
+        height:100%;
+        display:inline-block;
+        background: gray;
+    }
     .audiobox-control, .audiobox-control-interactive {
         top:0;
         max-height: 32px;
-        position: absolute;
         width: 100%;
+        display:inline-block;
         pointer-events: none;
     }
     .audiobox-control-interactive {
@@ -23,6 +30,5 @@
     }
     .audiobox-record-interactive {
         pointer-events: all;
-
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 85607c6b8..55b472726 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -38,8 +38,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     _linkPlayDisposer: IReactionDisposer | undefined;
     _reactionDisposer: IReactionDisposer | undefined;
-    _ref = React.createRef<HTMLAudioElement>();
+    _ele: HTMLAudioElement | null = null;
     _recorder: any;
+    _lastUpdate = 0;
 
     @observable private _audioState = 0;
     public static ActiveRecordings: Doc[] = [];
@@ -64,21 +65,44 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             });
     }
 
+    updateHighlights = () => {
+        const extensionDoc = this.extensionDoc;
+        const htmlEle = this._ele;
+        const start = extensionDoc && DateCast(extensionDoc.recordingStart);
+        if (htmlEle && !htmlEle.paused && start) {
+            setTimeout(this.updateHighlights, 30);
+            DocListCast(this.dataDoc.links).map(l => {
+                let la1 = l.anchor1 as Doc;
+                if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+                    la1 = l.anchor2 as Doc;
+                }
+                let date = DateCast(la1.creationDate);
+                let offset = (date!.date.getTime() - start.date.getTime()) / 1000;
+                if (offset > this._lastUpdate && offset < htmlEle.currentTime) {
+                    Doc.linkFollowHighlight(la1);
+                }
+            });
+            this._lastUpdate = htmlEle.currentTime;
+        }
+    }
+
     playFrom = (sel: Doc) => {
         const extensionDoc = this.extensionDoc;
         let start = extensionDoc && DateCast(extensionDoc.recordingStart);
-        let seek = sel && DateCast(sel.creationDate)
-        if (this._ref.current && start && seek) {
+        let seek = sel && DateCast(sel.creationDate);
+        if (this._ele && start && seek) {
             if (sel) {
                 let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
-                if (start && seek && delta > 0 && delta < this._ref.current.duration) {
-                    this._ref.current.currentTime = delta;
-                    this._ref.current.play();
+                if (start && seek && delta > 0 && delta < this._ele.duration) {
+                    this._ele.currentTime = delta;
+                    this._ele.play();
+                    this._lastUpdate = delta;
+                    setTimeout(this.updateHighlights, 0);
                 } else {
-                    this._ref.current.pause();
+                    this._ele.pause();
                 }
             } else {
-                this._ref.current.pause();
+                this._ele.pause();
             }
         }
     }
@@ -132,19 +156,22 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._audioState = 2;
         let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
         ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
-    })
+    });
 
     recordClick = (e: React.MouseEvent) => {
         if (e.button === 0 && !e.ctrlKey) {
-            if (this._recorder) {
-                this.stopRecording();
-            } else {
-                this.recordAudioAnnotation();
-            }
+            this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
             e.stopPropagation();
         }
     }
 
+    playClick = (e: any) => setTimeout(this.updateHighlights, 30);
+
+    setRef = (e: HTMLAudioElement | null) => {
+        e && e.addEventListener("play", this.playClick as any);
+        this._ele = e;
+    }
+
     @computed get path() {
         let field = Cast(this.props.Document[this.props.fieldKey], AudioField);
         let path = (field instanceof AudioField) ? field.url.href : "";
@@ -153,7 +180,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     @computed get audio() {
         let interactive = this.active() ? "-interactive" : "";
-        return <audio controls ref={this._ref} className={`audiobox-control${interactive}`}>
+        return <audio controls ref={this.setRef} className={`audiobox-control${interactive}`}>
             <source src={this.path} type="audio/mpeg" />
             Not supported.
         </audio>;
@@ -163,6 +190,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         let interactive = this.active() ? "-interactive" : "";
         return (!this.extensionDoc ? (null) :
             <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
+                <div className="audiobox-handle"></div>
                 {!this.path ?
                     <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}>
                         {this._audioState === 1 ? "STOP" : "RECORD"}
-- 
cgit v1.2.3-70-g09d2


From 77119dc1719af955e162248e5747bd9ef8921b4c Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 22 Oct 2019 20:43:24 -0400
Subject: added ink audio scrubbing

---
 src/client/documents/Documents.ts                   |  4 +---
 src/client/views/InkingCanvas.tsx                   | 11 +++++++----
 src/client/views/InkingControl.tsx                  |  1 +
 src/client/views/InkingStroke.tsx                   | 18 ++++++++++++------
 src/client/views/nodes/AudioBox.tsx                 | 21 +++++++++++++--------
 src/client/views/nodes/VideoBox.tsx                 |  2 --
 src/new_fields/InkField.ts                          |  4 +++-
 .../authentication/models/current_user_utils.ts     |  1 +
 8 files changed, 38 insertions(+), 24 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 316efe44c..5ae4ca82a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -334,9 +334,7 @@ export namespace Docs {
             let dataDoc = MakeDataDelegate(proto, protoProps, data);
             let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
 
-            AudioBox.ActiveRecordings.map(d => {
-                DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title);
-            });
+            AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
 
             return Doc.assign(viewDoc, delegateProps);
         }
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 920ebaedd..0037b95d0 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -78,7 +78,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
 
         this.previousState = new Map(this.inkData);
 
-        if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+        if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
             // start the new line, saves a uuid to represent the field of the stroke
             this._currentStrokeId = Utils.GenerateGuid();
             const data = this.inkData;
@@ -87,7 +87,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
                 color: InkingControl.Instance.selectedColor,
                 width: InkingControl.Instance.selectedWidth,
                 tool: InkingControl.Instance.selectedTool,
-                displayTimecode: NumCast(this.props.Document.currentTimecode, -1)
+                displayTimecode: NumCast(this.props.Document.currentTimecode, -1),
+                creationTime: new Date().getTime()
             });
             this.inkData = data;
         }
@@ -120,7 +121,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
     onPointerMove = (e: PointerEvent): void => {
         e.stopPropagation();
         e.preventDefault();
-        if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+        if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
             let data = this.inkData;  // add points to new line as it is being drawn
             let strokeData = data.get(this._currentStrokeId);
             if (strokeData) {
@@ -161,6 +162,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
                     color={strokeData.color}
                     width={strokeData.width}
                     tool={strokeData.tool}
+                    creationTime={strokeData.creationTime}
                     deleteCallback={this.removeLine} />);
             }
             return paths;
@@ -181,7 +183,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
 
     render() {
         let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
-        let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ? "pointer" : "default") : undefined;
+        let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ||
+            InkingControl.Instance.selectedTool === InkTool.Scrubber ? "pointer" : "default") : undefined;
         return (
             <div className="inkingCanvas">
                 <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} style={{ cursor: cursor }} />
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 105adc03d..75faa9641 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -126,6 +126,7 @@ export class InkingControl {
 Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
 Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
 Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
+Scripting.addGlobal(function activateScrubber(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Scrubber : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
 Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
 Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index b8d428d31..7bbf71482 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,9 +1,10 @@
 import { observer } from "mobx-react";
-import { observable, trace } from "mobx";
+import { observable, trace, runInAction } from "mobx";
 import { InkingControl } from "./InkingControl";
 import React = require("react");
 import { InkTool } from "../../new_fields/InkField";
 import "./InkingStroke.scss";
+import { AudioBox } from "./nodes/AudioBox";
 
 
 interface StrokeProps {
@@ -15,6 +16,7 @@ interface StrokeProps {
     color: string;
     width: string;
     tool: InkTool;
+    creationTime: number;
     deleteCallback: (index: string) => void;
 }
 
@@ -31,6 +33,11 @@ export class InkingStroke extends React.Component<StrokeProps> {
             e.stopPropagation();
             e.preventDefault();
         }
+        if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) {
+            runInAction(() => AudioBox.ScrubTime = this.props.creationTime);
+            e.stopPropagation();
+            e.preventDefault();
+        }
     }
 
     parseData = (line: Array<{ x: number, y: number }>): string => {
@@ -55,10 +62,9 @@ export class InkingStroke extends React.Component<StrokeProps> {
         let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
         let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
 
-        let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
-        return (
-            <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
-                onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
-        );
+        let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ||
+            InkingControl.Instance.selectedTool === InkTool.Scrubber ? "all" : "none";
+        return (<path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }}
+            strokeLinejoin="round" strokeLinecap="round" onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />);
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 55b472726..62ec683da 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -38,11 +38,13 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     _linkPlayDisposer: IReactionDisposer | undefined;
     _reactionDisposer: IReactionDisposer | undefined;
+    _scrubbingDisposer: IReactionDisposer | undefined;
     _ele: HTMLAudioElement | null = null;
     _recorder: any;
     _lastUpdate = 0;
 
     @observable private _audioState = 0;
+    @observable public static ScrubTime = 0;
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
@@ -53,7 +55,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     const la1 = l.anchor1 as Doc;
                     const la2 = l.anchor2 as Doc;
                     if (l[Id] === scrollLinkId && la1 && la2) {
-                        setTimeout(() => this.playFrom(Doc.AreProtosEqual(la1, this.dataDoc) ? la2 : la1), 250);
+                        let doc = Doc.AreProtosEqual(la1, this.dataDoc) ? la2 : la1;
+                        let seek = DateCast(la1.creationTime);
+                        setTimeout(() => this.playFrom(seek.date.getTime()), 250);
                     }
                 });
                 scrollLinkId && (this.layoutDoc.scrollLinkID = undefined);
@@ -61,8 +65,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             selected => {
                 let sel = selected.length ? selected[0].props.Document : undefined;
-                this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(sel);
+                this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
+        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, time => this.Document.playOnSelect && this.playFrom(time));
     }
 
     updateHighlights = () => {
@@ -86,14 +91,13 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         }
     }
 
-    playFrom = (sel: Doc) => {
+    playFrom = (seek: number) => {
         const extensionDoc = this.extensionDoc;
         let start = extensionDoc && DateCast(extensionDoc.recordingStart);
-        let seek = sel && DateCast(sel.creationDate);
-        if (this._ele && start && seek) {
-            if (sel) {
-                let delta = (seek.date.getTime() - start.date.getTime()) / 1000;
-                if (start && seek && delta > 0 && delta < this._ele.duration) {
+        if (this._ele && start) {
+            if (seek) {
+                let delta = (seek - start.date.getTime()) / 1000;
+                if (start && delta > 0 && delta < this._ele.duration) {
                     this._ele.currentTime = delta;
                     this._ele.play();
                     this._lastUpdate = delta;
@@ -110,6 +114,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     componentWillUnmount() {
         this._reactionDisposer && this._reactionDisposer();
         this._linkPlayDisposer && this._linkPlayDisposer();
+        this._scrubbingDisposer && this._scrubbingDisposer();
     }
 
     recordAudioAnnotation = () => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 48a699e58..53baea4ae 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -301,7 +301,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         this._isResetClick = 0;
         document.addEventListener("pointermove", this.onResetMove, true);
         document.addEventListener("pointerup", this.onResetUp, true);
-        InkingControl.Instance.switchTool(InkTool.Eraser);
     }
 
     onResetMove = (e: PointerEvent) => {
@@ -314,7 +313,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     onResetUp = (e: PointerEvent) => {
         document.removeEventListener("pointermove", this.onResetMove, true);
         document.removeEventListener("pointerup", this.onResetUp, true);
-        InkingControl.Instance.switchTool(InkTool.None);
         this._isResetClick < 10 && (this.Document.currentTimecode = 0);
     }
 
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index e381d0218..d94834e91 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -8,7 +8,8 @@ export enum InkTool {
     None,
     Pen,
     Highlighter,
-    Eraser
+    Eraser,
+    Scrubber
 }
 
 export interface StrokeData {
@@ -17,6 +18,7 @@ export interface StrokeData {
     width: string;
     tool: InkTool;
     displayTimecode: number;
+    creationTime: number;
 }
 
 export type InkData = Map<string, StrokeData>;
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 477b36ea4..95ebe3cb6 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -58,6 +58,7 @@ export class CurrentUserUtils {
             { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen,  this)`, activePen: doc },
             { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
             { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
+            { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
             { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this) && this.activePen.pen !== undefined`, backgroundColor: "white", activePen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
-- 
cgit v1.2.3-70-g09d2


From 3c8d5d0a53d03d570fd57789ecf43121eb814b0f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 23 Oct 2019 10:39:53 -0400
Subject: several fixes to audio to better support generic timecode linking

---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/views/nodes/AudioBox.tsx                | 63 +++++++++++++---------
 src/client/views/nodes/DocumentView.tsx            | 18 +++----
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 src/new_fields/documentSchemas.ts                  | 14 +++--
 .../authentication/models/current_user_utils.ts    | 14 ++---
 6 files changed, 67 insertions(+), 48 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5ae4ca82a..cad417d33 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -84,7 +84,7 @@ export interface DocumentOptions {
     columnWidth?: number;
     fontSize?: number;
     curPage?: number;
-    currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)
+    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;
@@ -104,7 +104,7 @@ 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
-    panel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
+    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.
     // [key: string]: Opt<Field>;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 62ec683da..135874400 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,7 +2,7 @@ import React = require("react");
 import { FieldViewProps, FieldView } from './FieldView';
 import { observer } from "mobx-react";
 import "./AudioBox.scss";
-import { Cast, DateCast } from "../../../new_fields/Types";
+import { Cast, DateCast, NumCast } from "../../../new_fields/Types";
 import { AudioField, nullAudio } from "../../../new_fields/URLField";
 import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface, createSchema } from "../../../new_fields/Schema";
@@ -41,33 +41,33 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _scrubbingDisposer: IReactionDisposer | undefined;
     _ele: HTMLAudioElement | null = null;
     _recorder: any;
+    _recordStart = 0;
     _lastUpdate = 0;
 
-    @observable private _audioState = 0;
+    @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
     @observable public static ScrubTime = 0;
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
-        runInAction(() => this._audioState = this.path ? 2 : 0);
+        runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
         this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
             scrollLinkId => {
-                scrollLinkId && DocListCast(this.dataDoc.links).map(l => {
-                    const la1 = l.anchor1 as Doc;
-                    const la2 = l.anchor2 as Doc;
-                    if (l[Id] === scrollLinkId && la1 && la2) {
-                        let doc = Doc.AreProtosEqual(la1, this.dataDoc) ? la2 : la1;
-                        let seek = DateCast(la1.creationTime);
-                        setTimeout(() => this.playFrom(seek.date.getTime()), 250);
-                    }
+                scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
+                    let la1 = l.anchor1 as Doc;
+                    let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
+                    setTimeout(() => this.playFrom(linkTime), 250);
                 });
-                scrollLinkId && (this.layoutDoc.scrollLinkID = undefined);
+                scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
             }, { fireImmediately: true });
         this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             selected => {
                 let sel = selected.length ? selected[0].props.Document : undefined;
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
-        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, time => this.Document.playOnSelect && this.playFrom(time));
+        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, timeInMillisecondsFrom1970 => {
+            let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+            start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
+        });
     }
 
     updateHighlights = () => {
@@ -76,14 +76,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         const start = extensionDoc && DateCast(extensionDoc.recordingStart);
         if (htmlEle && !htmlEle.paused && start) {
             setTimeout(this.updateHighlights, 30);
+            this.Document.currentTimecode = htmlEle.currentTime;
             DocListCast(this.dataDoc.links).map(l => {
                 let la1 = l.anchor1 as Doc;
+                let linkTime = NumCast(l.anchor2Timecode);
                 if (Doc.AreProtosEqual(la1, this.dataDoc)) {
                     la1 = l.anchor2 as Doc;
+                    linkTime = NumCast(l.anchor1Timecode);
                 }
-                let date = DateCast(la1.creationDate);
-                let offset = (date!.date.getTime() - start.date.getTime()) / 1000;
-                if (offset > this._lastUpdate && offset < htmlEle.currentTime) {
+                if (linkTime > this._lastUpdate && linkTime < htmlEle.currentTime) {
                     Doc.linkFollowHighlight(la1);
                 }
             });
@@ -91,16 +92,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         }
     }
 
-    playFrom = (seek: number) => {
+    playFrom = (seekTimeInSeconds: number) => {
         const extensionDoc = this.extensionDoc;
         let start = extensionDoc && DateCast(extensionDoc.recordingStart);
         if (this._ele && start) {
-            if (seek) {
-                let delta = (seek - start.date.getTime()) / 1000;
-                if (start && delta > 0 && delta < this._ele.duration) {
-                    this._ele.currentTime = delta;
+            if (seekTimeInSeconds) {
+                if (seekTimeInSeconds >= 0 && seekTimeInSeconds <= this._ele.duration) {
+                    this._ele.currentTime = seekTimeInSeconds;
                     this._ele.play();
-                    this._lastUpdate = delta;
+                    this._lastUpdate = seekTimeInSeconds;
                     setTimeout(this.updateHighlights, 0);
                 } else {
                     this._ele.pause();
@@ -117,6 +117,14 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._scrubbingDisposer && this._scrubbingDisposer();
     }
 
+
+    updateRecordTime = () => {
+        if (this._audioState === "recording") {
+            setTimeout(this.updateRecordTime, 30);
+            this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
+        }
+    }
+
     recordAudioAnnotation = () => {
         let gumStream: any;
         let self = this;
@@ -140,7 +148,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 // upload to server with known URL 
                 self.props.Document[self.props.fieldKey] = new AudioField(url);
             };
-            runInAction(() => self._audioState = 1);
+            runInAction(() => self._audioState = "recording");
+            self._recordStart = new Date().getTime();
+            setTimeout(self.updateRecordTime, 0);
             self._recorder.start();
             setTimeout(() => {
                 self.stopRecording();
@@ -158,7 +168,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     stopRecording = action(() => {
         this._recorder.stop();
-        this._audioState = 2;
+        this._audioState = "recorded";
         let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
         ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
     });
@@ -174,6 +184,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     setRef = (e: HTMLAudioElement | null) => {
         e && e.addEventListener("play", this.playClick as any);
+        e && e.addEventListener("timeupdate", () => this.props.Document.currentTimecode = e!.currentTime);
         this._ele = e;
     }
 
@@ -197,8 +208,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
                 <div className="audiobox-handle"></div>
                 {!this.path ?
-                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}>
-                        {this._audioState === 1 ? "STOP" : "RECORD"}
+                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+                        {this._audioState === "recording" ? "STOP" : "RECORD"}
                     </button> :
                     this.audio
                 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 67c85e158..c82fd0f0f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -165,7 +165,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 Doc.UnBrushDoc(this.props.Document);
             } else if (this.onClickHandler && this.onClickHandler.script) {
                 this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
-            } else if (this.props.Document.type === DocumentType.BUTTON) {
+            } else if (this.Document.type === DocumentType.BUTTON) {
                 ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
             } else if (this.Document.isButton) {
                 SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
@@ -179,8 +179,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
-        let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
-        let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+        let maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
+        let summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
         let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
         let expandedDocs: Doc[] = [];
         expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -333,9 +333,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     onDrop = (e: React.DragEvent) => {
         let text = e.dataTransfer.getData("text/plain");
         if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
-            let oldLayout = StrCast(this.props.Document.layout);
+            let oldLayout = this.Document.layout || "";
             let layout = text.replace("{layout}", oldLayout);
-            this.props.Document.layout = layout;
+            this.Document.layout = layout;
             e.stopPropagation();
             e.preventDefault();
         }
@@ -355,7 +355,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     makeIntoPortal = async () => {
-        let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
+        let anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
         if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
             let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
             DocServer.GetRefField(portalID).then(existingPortal => {
@@ -590,7 +590,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     render() {
         if (!this.props.Document) return (null);
-        let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined;
+        const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
         const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
         const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
         const colorSet = this.setsLayoutProp("backgroundColor");
@@ -644,7 +644,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
                 style={{
-                    transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
+                    transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
                     pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
                     color: StrCast(this.Document.color),
                     outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
@@ -658,7 +658,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
-                {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) =>
+                {this.Document.links && DocListCast(this.Document.links).map((d, i) =>
                     //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) :
                     <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
                         <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} />
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5c2d39d98..5588bb4c9 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -605,7 +605,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                         setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
                         setTimeout(() => this.unhighlightSearchTerms(), 2000);
                     }
-                    this.layoutDoc.scrollToLinkID = undefined;
+                    Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
                 }
 
             },
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 8c3b62067..e2730914f 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -1,9 +1,10 @@
 import { makeInterface, createSchema, listSpec } from "./Schema";
 import { ScriptField } from "./ScriptField";
 import { Doc } from "./Doc";
+import { DateField } from "./DateField";
 
 export const documentSchema = createSchema({
-    // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
+    layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below (see layoutCustom as an example)
     layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
     layoutCustom: 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)
@@ -14,7 +15,8 @@ export const documentSchema = createSchema({
     color: "string",            // foreground color of document
     backgroundColor: "string",  // background color of document
     opacity: "number",          // opacity of document
-    //links: listSpec(Doc),       // computed (readonly) list of links associated with this document
+    creationDate: DateField,     // when the document was created
+    links: listSpec(Doc),       // computed (readonly) list of links associated with this document
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
     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)
@@ -25,6 +27,9 @@ export const documentSchema = createSchema({
     isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
     isBackground: "boolean",    // whether document is a background element and ignores input events (can only selet with marquee)
     type: "string",             // enumerated type of document
+    currentTimecode: "number",   // current play back time of a temporal document (video / audio)
+    summarizedDocs: listSpec(Doc), // documents that are summarized by this document (and which will typically be opened by clicking this document)
+    maximizedDocs: listSpec(Doc), // documents to maximize when clicking this document (generally this document will be an icon)
     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 spatially manipulated
     inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
@@ -33,8 +38,11 @@ export const documentSchema = createSchema({
     heading: "number",          // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
     showCaption: "string",      // whether editable caption text is overlayed at the bottom of the document 
     showTitle: "string",        // whether an editable title banner is displayed at tht top of the document
-    isButton: "boolean",        // whether document functions as a button (overiding native interactions of its content)      
+    isButton: "boolean",        // whether document functions as a button (overiding native interactions of its content)    
     ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
+    isAnimating: "boolean",     // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).
+    animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards 
+    scrollToLinkID: "string",   // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
 });
 
 export const positionSchema = createSchema({
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 95ebe3cb6..044ec746b 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -83,10 +83,10 @@ export class CurrentUserUtils {
 
         return Docs.Create.ButtonDocument({
             width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create", targetContainer: sidebarContainer,
-            panel: Docs.Create.StackingDocument([dragCreators, color], {
+            sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
                 width: 500, height: 800, chromeStatus: "disabled", title: "creator stack"
             }),
-            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel"),
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
         });
     }
 
@@ -108,11 +108,11 @@ export class CurrentUserUtils {
 
         return Docs.Create.ButtonDocument({
             width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library",
-            panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
+            sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
                 title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true
             }),
             targetContainer: sidebarContainer,
-            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
         });
     }
 
@@ -120,11 +120,11 @@ export class CurrentUserUtils {
     static setupSearchPanel(sidebarContainer: Doc) {
         return Docs.Create.ButtonDocument({
             width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search",
-            panel: Docs.Create.QueryDocument({
+            sourcePanel: Docs.Create.QueryDocument({
                 title: "search stack", ignoreClick: true
             }),
             targetContainer: sidebarContainer,
-            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
         });
     }
 
@@ -190,7 +190,7 @@ export class CurrentUserUtils {
             stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => {
                 sidebarButtons && sidebarButtons.map((sidebarBtn, i) => {
                     sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => {
-                        btn && btn.panel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
+                        btn && btn.sourcePanel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
                     });
                 });
             });
-- 
cgit v1.2.3-70-g09d2


From 86e985ef3a96d9849799e436d2158fef0b2b7f94 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 23 Oct 2019 11:45:08 -0400
Subject: added scrubbing from text notes

---
 src/client/views/InkingStroke.tsx           | 2 +-
 src/client/views/nodes/AudioBox.tsx         | 5 +++--
 src/client/views/nodes/FormattedTextBox.tsx | 3 +++
 3 files changed, 7 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 7bbf71482..332c22512 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -34,7 +34,7 @@ export class InkingStroke extends React.Component<StrokeProps> {
             e.preventDefault();
         }
         if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) {
-            runInAction(() => AudioBox.ScrubTime = this.props.creationTime);
+            AudioBox.SetScrubTime(this.props.creationTime);
             e.stopPropagation();
             e.preventDefault();
         }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 135874400..e9a11c9b6 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -45,7 +45,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _lastUpdate = 0;
 
     @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
-    @observable public static ScrubTime = 0;
+    @observable private static _scrubTime = 0;
+    public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
@@ -64,7 +65,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 let sel = selected.length ? selected[0].props.Document : undefined;
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
-        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, timeInMillisecondsFrom1970 => {
+        this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
             let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
             start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
         });
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5588bb4c9..566b698bd 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -45,6 +45,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { documentSchema } from '../../../new_fields/documentSchemas';
+import { AudioBox } from './AudioBox';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -190,6 +191,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
 
+            let tsel = this._editorView.state.selection.$from;
+            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 5000)));
             this._applyingChange = true;
             this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
             this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
-- 
cgit v1.2.3-70-g09d2


From c861f31a93ad246b9200b328a63df15da795f050 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 23 Oct 2019 17:26:41 -0400
Subject: slight improvements to audio ui

---
 src/client/views/MainView.tsx                      |  1 +
 src/client/views/nodes/AudioBox.scss               | 19 +++++++++
 src/client/views/nodes/AudioBox.tsx                | 48 +++++++++++++++-------
 .../authentication/models/current_user_utils.ts    |  1 +
 4 files changed, 55 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 78b8ac0b7..cc5c5bf2b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -37,6 +37,7 @@ import { OverlayView } from './OverlayView';
 import PDFMenu from './pdf/PDFMenu';
 import { PreviewCursor } from './PreviewCursor';
 import { Scripting } from '../util/Scripting';
+import { LinkManager } from '../util/LinkManager';
 
 @observer
 export class MainView extends React.Component {
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 9bda5b2a7..14b90c570 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -31,4 +31,23 @@
     .audiobox-record-interactive {
         pointer-events: all;
     }
+    .audiobox-controls {
+        width:100%;
+        height:100%;
+        position: relative;
+        display: grid;
+        .audiobox-player {
+            margin-top:auto;
+            margin-bottom:auto;
+            width:100%;
+            position: relative;
+            display: grid;
+            .audiobox-marker {
+                position:absolute;
+                width:10px;
+                height:100%;
+                background:green;
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index e9a11c9b6..57360272e 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -16,6 +16,8 @@ import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ContextMenu } from "../ContextMenu";
 import { Id } from "../../../new_fields/FieldSymbols";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { DocumentManager } from "../../util/DocumentManager";
 
 interface Window {
     MediaRecorder: MediaRecorder;
@@ -42,6 +44,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _ele: HTMLAudioElement | null = null;
     _recorder: any;
     _recordStart = 0;
+    @observable _duration = 1;
     _lastUpdate = 0;
 
     @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
@@ -50,6 +53,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
+        runInAction(() => this._duration = NumCast(this.dataDoc.duration, 1));
         runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
         this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
             scrollLinkId => {
@@ -76,6 +80,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         const htmlEle = this._ele;
         const start = extensionDoc && DateCast(extensionDoc.recordingStart);
         if (htmlEle && !htmlEle.paused && start) {
+            htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = this._duration = htmlEle.duration);
             setTimeout(this.updateHighlights, 30);
             this.Document.currentTimecode = htmlEle.currentTime;
             DocListCast(this.dataDoc.links).map(l => {
@@ -94,18 +99,14 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     playFrom = (seekTimeInSeconds: number) => {
-        const extensionDoc = this.extensionDoc;
-        let start = extensionDoc && DateCast(extensionDoc.recordingStart);
-        if (this._ele && start) {
-            if (seekTimeInSeconds) {
-                if (seekTimeInSeconds >= 0 && seekTimeInSeconds <= this._ele.duration) {
-                    this._ele.currentTime = seekTimeInSeconds;
-                    this._ele.play();
-                    this._lastUpdate = seekTimeInSeconds;
-                    setTimeout(this.updateHighlights, 0);
-                } else {
-                    this._ele.pause();
-                }
+        if (this._ele) {
+            if (seekTimeInSeconds < 0) {
+                this._ele.pause();
+            } else if (seekTimeInSeconds <= this._ele.duration) {
+                this._ele.currentTime = seekTimeInSeconds;
+                this._ele.play();
+                this._lastUpdate = seekTimeInSeconds;
+                setTimeout(this.updateHighlights, 0);
             } else {
                 this._ele.pause();
             }
@@ -182,6 +183,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     playClick = (e: any) => setTimeout(this.updateHighlights, 30);
+    onPlay = (e: any) => this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
 
     setRef = (e: HTMLAudioElement | null) => {
         e && e.addEventListener("play", this.playClick as any);
@@ -197,7 +199,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     @computed get audio() {
         let interactive = this.active() ? "-interactive" : "";
-        return <audio controls ref={this.setRef} className={`audiobox-control${interactive}`}>
+        return <audio ref={this.setRef} className={`audiobox-control${interactive}`}>
             <source src={this.path} type="audio/mpeg" />
             Not supported.
         </audio>;
@@ -212,7 +214,25 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
                         {this._audioState === "recording" ? "STOP" : "RECORD"}
                     </button> :
-                    this.audio
+                    <div className="audiobox-controls">
+                        <div className="audiobox-player" onClick={this.onPlay}>
+                            <FontAwesomeIcon icon="play" size="sm" ></FontAwesomeIcon>
+                            {DocListCast(this.dataDoc.links).map((l, i) => {
+                                let la1 = l.anchor1 as Doc;
+                                let la2 = l.anchor2 as Doc;
+                                let linkTime = NumCast(l.anchor2Timecode);
+                                if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+                                    la1 = l.anchor2 as Doc;
+                                    la2 = l.anchor1 as Doc;
+                                    linkTime = NumCast(l.anchor1Timecode);
+                                }
+                                return <div key={i} className="audiobox-marker" onPointerDown={() =>
+                                    DocumentManager.Instance.FollowLink(l, la2, document => this.props.addDocTab(document, undefined, "onRight"), false)}
+                                    style={{ left: `${linkTime / this._duration * 100}%` }} />;
+                            })}
+                            {this.audio}
+                        </div>
+                    </div>
                 }
             </div>
         );
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 044ec746b..ee9794564 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -15,6 +15,7 @@ import { RouteStore } from "../../RouteStore";
 import { InkingControl } from "../../../client/views/InkingControl";
 import { DragManager } from "../../../client/util/DragManager";
 import { nullAudio } from "../../../new_fields/URLField";
+import { LinkManager } from "../../../client/util/LinkManager";
 
 export class CurrentUserUtils {
     private static curr_id: string;
-- 
cgit v1.2.3-70-g09d2


From 910e4d8a22a312d2ca0b8a970ff434604f7c6f91 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 23 Oct 2019 23:35:01 -0400
Subject: several fixes to audio documents to make linking work better and to
 customize the audio controls UI

---
 src/client/documents/Documents.ts       |  3 +-
 src/client/util/DocumentManager.ts      |  4 +-
 src/client/views/nodes/AudioBox.scss    | 62 +++++++++++++++++++---
 src/client/views/nodes/AudioBox.tsx     | 94 +++++++++++++++++++++------------
 src/client/views/nodes/DocuLinkBox.tsx  |  4 +-
 src/client/views/nodes/DocumentView.tsx | 16 ++++--
 src/new_fields/Doc.ts                   |  2 +
 7 files changed, 134 insertions(+), 51 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cad417d33..f26594e04 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -171,7 +171,7 @@ export namespace Docs {
             }],
             [DocumentType.AUDIO, {
                 layout: { view: AudioBox, dataField: data },
-                options: { height: 32 }
+                options: { height: 35, backgroundColor: "lightGray", borderRounding: "20%" }
             }],
             [DocumentType.PDF, {
                 layout: { view: PDFBox, dataField: data },
@@ -691,6 +691,7 @@ export namespace DocUtils {
             }
         });
     }
+
     export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
         let sv = DocumentManager.Instance.getDocumentView(source.doc);
         if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 0f2a47dd0..346e88f40 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -101,10 +101,10 @@ export class DocumentManager {
     @computed
     public get LinkedDocumentViews() {
         let pairs = DocumentManager.Instance.DocumentViews.filter(dv =>
-            dv.isSelected() || Doc.IsBrushed(dv.props.Document)  // draw links from DocumentViews that are selected or brushed OR
+            (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR
             || DocumentManager.Instance.DocumentViews.some(dv2 => {                                                  // Documentviews which
                 let rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors 
-                let init = dv2.isSelected() || Doc.IsBrushed(dv2.props.Document);                                    // on a view that is selected or brushed
+                let init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO;  // on a view that is selected or brushed
                 return init && rest;
             })
         ).reduce((pairs, dv) => {
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 14b90c570..3d6f6c4f9 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,10 +1,10 @@
 .audiobox-container, .audiobox-container-interactive {
     width: 100%;
     height: 100%;
-    min-height: 32px;
     position: inherit;
     display:flex;
     pointer-events: all;
+    cursor:default;
     .audiobox-handle {
         width:20px;
         height:100%;
@@ -35,18 +35,66 @@
         width:100%;
         height:100%;
         position: relative;
-        display: grid;
+        display: flex;
         .audiobox-player {
             margin-top:auto;
             margin-bottom:auto;
             width:100%;
+            height: 80%;
             position: relative;
-            display: grid;
-            .audiobox-marker {
-                position:absolute;
-                width:10px;
+            display: flex;
+            .audiobox-playhead {
+                position: relative;
+                margin-top: auto;
+                margin-bottom: auto;
+                width: 25px;
+            }
+            .audiobox-timeline {
+                position:relative;
                 height:100%;
-                background:green;
+                width:100%;
+                .audiobox-current {
+                    width: 1px;
+                    height:100%;
+                    background-color: red;
+                    position: absolute;;
+                }
+                .audiobox-linker {
+                    position:absolute;
+                    width:15px;
+                    min-height:10px;
+                    height:15px;
+                    margin-left:-2.55px;
+                    background:gray;
+                    border-radius: 100%;
+                    background-color: transparent;
+                    box-shadow: black 2px 2px 1px;
+                    .docuLinkBox-cont {
+                        width:15px !important;
+                        height:15px !important;
+                        left: calc(100% - 15px) !important;
+                        top: calc(100% - 15px) !important;
+                    }
+                }
+                .audiobox-linker:hover {
+                    transform:scale(1.5);
+                }
+                .audiobox-marker-container {
+                    position:absolute;
+                    width:10px;
+                    height:100%;
+                    background:gray;
+                    border-radius: 5px;
+                    box-shadow: black 2px 2px 1px;
+                    .audiobox-marker {
+                        position:relative;
+                        height: calc(100% - 15px);
+                        margin-top: 15px;
+                    }
+                    .audio-marker:hover {
+                        border: orange 2px solid;
+                    }
+                }
             }
         }
     }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 57360272e..3933c6257 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,17 +7,18 @@ import { AudioField, nullAudio } from "../../../new_fields/URLField";
 import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface, createSchema } from "../../../new_fields/Schema";
 import { documentSchema } from "../../../new_fields/documentSchemas";
-import { Utils } from "../../../Utils";
+import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
 import { DateField } from "../../../new_fields/DateField";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, WidthSym } from "../../../new_fields/Doc";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ContextMenu } from "../ContextMenu";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentView } from "./DocumentView";
 
 interface Window {
     MediaRecorder: MediaRecorder;
@@ -44,16 +45,14 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _ele: HTMLAudioElement | null = null;
     _recorder: any;
     _recordStart = 0;
-    @observable _duration = 1;
-    _lastUpdate = 0;
 
-    @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
     @observable private static _scrubTime = 0;
+    @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
+    @observable private _playing = false;
     public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
-        runInAction(() => this._duration = NumCast(this.dataDoc.duration, 1));
         runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
         this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
             scrollLinkId => {
@@ -75,14 +74,12 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         });
     }
 
-    updateHighlights = () => {
+    timecodeChanged = () => {
         const extensionDoc = this.extensionDoc;
         const htmlEle = this._ele;
         const start = extensionDoc && DateCast(extensionDoc.recordingStart);
-        if (htmlEle && !htmlEle.paused && start) {
-            htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = this._duration = htmlEle.duration);
-            setTimeout(this.updateHighlights, 30);
-            this.Document.currentTimecode = htmlEle.currentTime;
+        if (start && htmlEle) {
+            htmlEle && htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
             DocListCast(this.dataDoc.links).map(l => {
                 let la1 = l.anchor1 as Doc;
                 let linkTime = NumCast(l.anchor2Timecode);
@@ -90,25 +87,29 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     la1 = l.anchor2 as Doc;
                     linkTime = NumCast(l.anchor1Timecode);
                 }
-                if (linkTime > this._lastUpdate && linkTime < htmlEle.currentTime) {
+                if (linkTime > NumCast(this.Document.currentTimecode) && linkTime < htmlEle.currentTime) {
                     Doc.linkFollowHighlight(la1);
                 }
             });
-            this._lastUpdate = htmlEle.currentTime;
+            this.Document.currentTimecode = htmlEle.currentTime;
         }
     }
 
+    pause = action(() => {
+        this._ele!.pause();
+        this._playing = false;
+    })
+
     playFrom = (seekTimeInSeconds: number) => {
         if (this._ele) {
             if (seekTimeInSeconds < 0) {
-                this._ele.pause();
+                this.pause();
             } else if (seekTimeInSeconds <= this._ele.duration) {
                 this._ele.currentTime = seekTimeInSeconds;
                 this._ele.play();
-                this._lastUpdate = seekTimeInSeconds;
-                setTimeout(this.updateHighlights, 0);
+                runInAction(() => this._playing = true);
             } else {
-                this._ele.pause();
+                this.pause();
             }
         }
     }
@@ -170,6 +171,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     stopRecording = action(() => {
         this._recorder.stop();
+        this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
         this._audioState = "recorded";
         let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
         ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
@@ -182,12 +184,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         }
     }
 
-    playClick = (e: any) => setTimeout(this.updateHighlights, 30);
     onPlay = (e: any) => this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+    onStop = (e: any) => {
+        this.pause();
+        this._ele!.currentTime = 0;
+        e.stopPropagation();
+    }
 
     setRef = (e: HTMLAudioElement | null) => {
-        e && e.addEventListener("play", this.playClick as any);
-        e && e.addEventListener("timeupdate", () => this.props.Document.currentTimecode = e!.currentTime);
+        e && e.addEventListener("timeupdate", action(() => this.Document.currentTimecode = e!.currentTime));
         this._ele = e;
     }
 
@@ -216,21 +221,40 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     </button> :
                     <div className="audiobox-controls">
                         <div className="audiobox-player" onClick={this.onPlay}>
-                            <FontAwesomeIcon icon="play" size="sm" ></FontAwesomeIcon>
-                            {DocListCast(this.dataDoc.links).map((l, i) => {
-                                let la1 = l.anchor1 as Doc;
-                                let la2 = l.anchor2 as Doc;
-                                let linkTime = NumCast(l.anchor2Timecode);
-                                if (Doc.AreProtosEqual(la1, this.dataDoc)) {
-                                    la1 = l.anchor2 as Doc;
-                                    la2 = l.anchor1 as Doc;
-                                    linkTime = NumCast(l.anchor1Timecode);
-                                }
-                                return <div key={i} className="audiobox-marker" onPointerDown={() =>
-                                    DocumentManager.Instance.FollowLink(l, la2, document => this.props.addDocTab(document, undefined, "onRight"), false)}
-                                    style={{ left: `${linkTime / this._duration * 100}%` }} />;
-                            })}
-                            {this.audio}
+                            <FontAwesomeIcon className="audiobox-playhead" icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 25 ? "1x" : "2x"} />
+                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon className="audiobox-playhead" icon="stop" size={this.props.PanelHeight() < 25 ? "1x" : "2x"} /></div>
+                            <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
+                                onPointerDown={e => {
+                                    if (e.button === 0 && !e.ctrlKey) {
+                                        let rect = (e.target as any).getBoundingClientRect();
+                                        this._ele!.currentTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+                                        this.pause();
+                                        e.stopPropagation();
+                                    }
+                                }} >
+                                {DocListCast(this.dataDoc.links).map((l, i) => {
+                                    let la1 = l.anchor1 as Doc;
+                                    let la2 = l.anchor2 as Doc;
+                                    let linkTime = NumCast(l.anchor2Timecode);
+                                    if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+                                        la1 = l.anchor2 as Doc;
+                                        la2 = l.anchor1 as Doc;
+                                        linkTime = NumCast(l.anchor1Timecode);
+                                    }
+                                    return !linkTime ? (null) : <div className="audiobox-marker-container" style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+                                        <div className="audioBox-linker" key={"linker" + i}>
+                                            <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+                                                parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+                                                backgroundColor={returnTransparent} />
+                                        </div>
+                                        <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+                                            onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
+                                            onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+                                    </div>;
+                                })}
+                                <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+                                {this.audio}
+                            </div>
                         </div>
                     </div>
                 }
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index 7119b0db0..f2ab6fcd8 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -33,7 +33,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         document.removeEventListener("pointerup", this.onPointerUp);
         document.addEventListener("pointermove", this.onPointerMove);
         document.addEventListener("pointerup", this.onPointerUp);
-        e.stopPropagation();
+        (e.button === 0 && !e.ctrlKey) && e.stopPropagation();
     }
     onPointerMove = action((e: PointerEvent) => {
         let cdiv = this._ref.current!.parentElement;
@@ -43,7 +43,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
             let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
             let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy));
             if (separation > 100) {
-                DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], this.props.ContainingCollectionDoc as Doc, this.props.Document); // Containging collection is the document, not a collection... hack.
+                DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, this.props.Document); // Containging collection is the document, not a collection... hack.
                 document.removeEventListener("pointermove", this.onPointerMove);
                 document.removeEventListener("pointerup", this.onPointerUp);
             } else if (dragdist > separation) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c82fd0f0f..ed93aa83e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,6 +41,7 @@ import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
+import { link } from 'fs';
 
 library.add(fa.faEdit);
 library.add(fa.faTrash);
@@ -585,8 +586,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             layoutKey={this.props.layoutKey || "layout"}
             DataDoc={this.props.DataDoc} />);
     }
-    linkEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2";
-    linkEndpointDoc = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? Cast(linkDoc.anchor1, Doc) as Doc : Cast(linkDoc.anchor2, Doc) as Doc;
+    linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
+
+    // used to decide whether a link document should be created or not. 
+    // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
+    // would be good to generalize this some way.
+    isNonTemporalLink = (linkDoc: Doc) => {
+        let anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
+        let ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode;
+        return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true;
+    }
 
     render() {
         if (!this.props.Document) return (null);
@@ -658,8 +667,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
-                {this.Document.links && DocListCast(this.Document.links).map((d, i) =>
-                    //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) :
+                {this.Document.links && DocListCast(this.Document.links).filter(this.isNonTemporalLink).map((d, i) =>
                     <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
                         <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} />
                     </div>)}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 08cb66d5f..6aad4a6be 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -669,6 +669,8 @@ export namespace Doc {
         return doc;
     }
 
+    export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
+
     export function linkFollowUnhighlight() {
         Doc.UnhighlightAll();
         document.removeEventListener("pointerdown", linkFollowUnhighlight);
-- 
cgit v1.2.3-70-g09d2


From 31166219e473e105b8fd9d49627fd1df58822c55 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 24 Oct 2019 00:56:00 -0400
Subject: fixed audio box shrinking layout.  fixed pdf z-index issue in
 stacking panels.

---
 src/client/views/nodes/AudioBox.scss | 31 ++++++++++++++++++++++++++++---
 src/client/views/nodes/AudioBox.tsx  | 16 ++++++++++------
 src/client/views/nodes/PDFBox.scss   |  1 -
 3 files changed, 38 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 3d6f6c4f9..ccbf0d75f 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -36,6 +36,8 @@
         height:100%;
         position: relative;
         display: flex;
+        padding-left: 2px;
+        border: gray solid 3px;
         .audiobox-player {
             margin-top:auto;
             margin-bottom:auto;
@@ -59,7 +61,7 @@
                     background-color: red;
                     position: absolute;;
                 }
-                .audiobox-linker {
+                .audiobox-linker, .audiobox-linker-mini {
                     position:absolute;
                     width:15px;
                     min-height:10px;
@@ -76,10 +78,24 @@
                         top: calc(100% - 15px) !important;
                     }
                 }
-                .audiobox-linker:hover {
+                .audiobox-linker-mini {
+                    width:8px;
+                    min-height:8px;
+                    height:8px;
+                    box-shadow: black 1px 1px 1px;
+                    margin-left: -1;
+                    margin-top: -2;
+                    .docuLinkBox-cont {
+                        width:8px !important;
+                        height:8px !important;
+                        left: calc(100% - 8px) !important;
+                        top: calc(100% - 8px) !important;
+                    }
+                }
+                .audiobox-linker:hover, .audiobox-linker-mini:hover {
                     transform:scale(1.5);
                 }
-                .audiobox-marker-container {
+                .audiobox-marker-container, .audiobox-marker-minicontainer {
                     position:absolute;
                     width:10px;
                     height:100%;
@@ -95,6 +111,15 @@
                         border: orange 2px solid;
                     }
                 }
+                .audiobox-marker-minicontainer {
+                    width:5px;
+                    border-radius: 1px;
+                    .audiobox-marker {
+                        position:relative;
+                        height: calc(100% - 8px);
+                        margin-top: 8px;
+                    }
+                }
             }
         }
     }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 3933c6257..cc1c63d44 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -184,7 +184,10 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         }
     }
 
-    onPlay = (e: any) => this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+    onPlay = (e: any) => {
+        this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+        e.stopPropagation();
+    }
     onStop = (e: any) => {
         this.pause();
         this._ele!.currentTime = 0;
@@ -213,7 +216,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     render() {
         let interactive = this.active() ? "-interactive" : "";
         return (!this.extensionDoc ? (null) :
-            <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
+            <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
+                onClick={!this.path ? this.recordClick : undefined}>
                 <div className="audiobox-handle"></div>
                 {!this.path ?
                     <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
@@ -221,8 +225,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     </button> :
                     <div className="audiobox-controls">
                         <div className="audiobox-player" onClick={this.onPlay}>
-                            <FontAwesomeIcon className="audiobox-playhead" icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 25 ? "1x" : "2x"} />
-                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon className="audiobox-playhead" icon="stop" size={this.props.PanelHeight() < 25 ? "1x" : "2x"} /></div>
+                            <FontAwesomeIcon className="audiobox-playhead" icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon className="audiobox-playhead" icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
                             <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
                                 onPointerDown={e => {
                                     if (e.button === 0 && !e.ctrlKey) {
@@ -241,8 +245,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                                         la2 = l.anchor1 as Doc;
                                         linkTime = NumCast(l.anchor1Timecode);
                                     }
-                                    return !linkTime ? (null) : <div className="audiobox-marker-container" style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
-                                        <div className="audioBox-linker" key={"linker" + i}>
+                                    return !linkTime ? (null) : <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+                                        <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
                                             <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
                                                 parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
                                                 backgroundColor={returnTransparent} />
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index deb98dc8d..8bec4fbe3 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -6,7 +6,6 @@
     width:100%;
     overflow: hidden;
     position:absolute;
-    z-index: -1;
     cursor:auto;
 }
 
-- 
cgit v1.2.3-70-g09d2


From c97b98c165e318da787361b8fc11382b0b98afa4 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 24 Oct 2019 11:54:03 -0400
Subject: several fixes to audioboxes.

---
 src/client/documents/Documents.ts                  |  3 ++-
 .../collections/CollectionMasonryViewFieldRow.tsx  |  2 +-
 .../views/collections/CollectionStackingView.scss  |  5 ++++
 .../CollectionFreeFormLinkView.scss                |  1 +
 .../CollectionFreeFormLinkView.tsx                 |  3 +++
 src/client/views/nodes/AudioBox.scss               | 28 ++++++++++++++--------
 src/client/views/nodes/AudioBox.tsx                | 19 +++++++--------
 src/client/views/nodes/DocuLinkBox.tsx             |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 9 files changed, 40 insertions(+), 25 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f26594e04..d1fcabc4a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -171,7 +171,7 @@ export namespace Docs {
             }],
             [DocumentType.AUDIO, {
                 layout: { view: AudioBox, dataField: data },
-                options: { height: 35, backgroundColor: "lightGray", borderRounding: "20%" }
+                options: { height: 35, backgroundColor: "lightGray" }
             }],
             [DocumentType.PDF, {
                 layout: { view: PDFBox, dataField: data },
@@ -714,6 +714,7 @@ export namespace DocUtils {
             linkDocProto.anchor2Timecode = target.doc.currentTimecode;
             linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
             linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
+            linkDocProto.borderRounding = "20";
             linkDocProto.width = linkDocProto.height = 0;
             linkDocProto.isBackground = true;
             linkDocProto.isButton = true;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 4259e948b..0249b90b4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -366,7 +366,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
                         >
                             {headingView}
                             {collapsed ? (null) :
-                                < div >
+                                < div style={{ position: "relative" }}>
                                     <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
                                         ref={this._contRef}
                                         style={{
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 3f4b678c7..1ff2275d4 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -21,6 +21,11 @@
     overflow-y: auto;
     flex-wrap: wrap;
     transition: top .5s;
+    >div {
+        height: 100%;
+        position: relative;
+        display: inline-block;
+    }
 
     .collectionSchemaView-previewDoc {
         height: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 1f1bca2f2..75af11537 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -3,6 +3,7 @@
     opacity: 0.8;
     pointer-events: all;
     stroke-width: 3px;
+    transition: opacity 0.5s ease-in;
 }
 .collectionfreeformlinkview-linkCircle {
     stroke: rgb(0,0,0);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 962fe2a1c..837413842 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -17,10 +17,12 @@ export interface CollectionFreeFormLinkViewProps {
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
     @observable _alive: number = 0;
+    @observable _opacity: number = 1;
     @action
     componentDidMount() {
         this._alive = 1;
         setTimeout(this.rerender, 50);
+        setTimeout(action(() => this._opacity = 0.05), 50);
     }
     @action
     componentWillUnmount() {
@@ -42,6 +44,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2);
         let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
         return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
+            style={{ opacity: this._opacity }}
             x1={`${pt1[0]}`} y1={`${pt1[1]}`}
             x2={`${pt2[0]}`} y2={`${pt2[1]}`} />);
     }
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index ccbf0d75f..3b19a6dba 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -44,22 +44,27 @@
             width:100%;
             height: 80%;
             position: relative;
+            padding-right: 5px;
             display: flex;
             .audiobox-playhead {
                 position: relative;
                 margin-top: auto;
                 margin-bottom: auto;
                 width: 25px;
+                padding: 2px;
             }
             .audiobox-timeline {
                 position:relative;
                 height:100%;
                 width:100%;
+                background: white;
+                border: gray solid 1px;
+                border-radius: 3px;
                 .audiobox-current {
                     width: 1px;
                     height:100%;
                     background-color: red;
-                    position: absolute;;
+                    position: absolute;
                 }
                 .audiobox-linker, .audiobox-linker-mini {
                     position:absolute;
@@ -72,10 +77,11 @@
                     background-color: transparent;
                     box-shadow: black 2px 2px 1px;
                     .docuLinkBox-cont {
-                        width:15px !important;
-                        height:15px !important;
-                        left: calc(100% - 15px) !important;
-                        top: calc(100% - 15px) !important;
+                        position: relative !important;
+                        height: 100%  !important;
+                        width: 100% !important;
+                        left:unset !important;
+                        top:unset !important;
                     }
                 }
                 .audiobox-linker-mini {
@@ -86,10 +92,11 @@
                     margin-left: -1;
                     margin-top: -2;
                     .docuLinkBox-cont {
-                        width:8px !important;
-                        height:8px !important;
-                        left: calc(100% - 8px) !important;
-                        top: calc(100% - 8px) !important;
+                        position: relative !important;
+                        height: 100%  !important;
+                        width: 100% !important;
+                        left:unset !important;
+                        top:unset !important;
                     }
                 }
                 .audiobox-linker:hover, .audiobox-linker-mini:hover {
@@ -98,7 +105,8 @@
                 .audiobox-marker-container, .audiobox-marker-minicontainer {
                     position:absolute;
                     width:10px;
-                    height:100%;
+                    height:90%;
+                    top:2.5%;
                     background:gray;
                     border-radius: 5px;
                     box-shadow: black 2px 2px 1px;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index cc1c63d44..bd1caaf1b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -12,12 +12,11 @@ import { RouteStore } from "../../../server/RouteStore";
 import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
 import { DateField } from "../../../new_fields/DateField";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Doc, DocListCast, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ContextMenu } from "../ContextMenu";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { DocumentManager } from "../../util/DocumentManager";
 import { DocumentView } from "./DocumentView";
 
 interface Window {
@@ -59,7 +58,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
                     let la1 = l.anchor1 as Doc;
                     let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
-                    setTimeout(() => this.playFrom(linkTime), 250);
+                    setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250);
                 });
                 scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
             }, { fireImmediately: true });
@@ -75,11 +74,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     timecodeChanged = () => {
-        const extensionDoc = this.extensionDoc;
         const htmlEle = this._ele;
-        const start = extensionDoc && DateCast(extensionDoc.recordingStart);
-        if (start && htmlEle) {
-            htmlEle && htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
+        if (this._audioState === "recorded" && htmlEle) {
+            htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
             DocListCast(this.dataDoc.links).map(l => {
                 let la1 = l.anchor1 as Doc;
                 let linkTime = NumCast(l.anchor2Timecode);
@@ -195,7 +192,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     setRef = (e: HTMLAudioElement | null) => {
-        e && e.addEventListener("timeupdate", action(() => this.Document.currentTimecode = e!.currentTime));
+        e && e.addEventListener("timeupdate", this.timecodeChanged);
         this._ele = e;
     }
 
@@ -225,13 +222,13 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     </button> :
                     <div className="audiobox-controls">
                         <div className="audiobox-player" onClick={this.onPlay}>
-                            <FontAwesomeIcon className="audiobox-playhead" icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
-                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon className="audiobox-playhead" icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                            <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
                             <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
                                 onPointerDown={e => {
                                     if (e.button === 0 && !e.ctrlKey) {
                                         let rect = (e.target as any).getBoundingClientRect();
-                                        this._ele!.currentTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+                                        this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
                                         this.pause();
                                         e.stopPropagation();
                                     }
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index f2ab6fcd8..8f68effe8 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -73,7 +73,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         let c = StrCast(this.props.Document.backgroundColor, "lightblue");
         return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.Document[this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"]! as Doc).title)}
             ref={this._ref} style={{
-                background: c, width: "25px", left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+                background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
                 transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})`
             }} />;
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 566b698bd..cf55a10ba 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -192,7 +192,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
             }
 
             let tsel = this._editorView.state.selection.$from;
-            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 5000)));
+            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
             this._applyingChange = true;
             this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
             this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
-- 
cgit v1.2.3-70-g09d2


From cf9fff0cf86e120cd4d26169ce8455bd8b438b24 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 24 Oct 2019 14:01:35 -0400
Subject: minor fixes including fix to focus() to restore animation panning

---
 src/client/views/collections/CollectionSubView.tsx        |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx         | 15 +++++++--------
 src/client/views/nodes/AudioBox.tsx                       |  2 +-
 src/client/views/nodes/DocuLinkBox.tsx                    |  3 ++-
 src/client/views/nodes/DocumentView.tsx                   |  4 ++--
 5 files changed, 13 insertions(+), 13 deletions(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 55365de8c..d7e9494a3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -74,7 +74,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             this._childLayoutDisposer && this._childLayoutDisposer();
         }
 
-        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
 
         // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f6518a01d..6e0f75bc1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -354,14 +354,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @action
-    setPan(panX: number, panY: number) {
-        if (!this.props.Document.lockedPosition || this.props.Document.inOverlay) {
-            this.props.Document.panTransformType = "None";
+    setPan(panX: number, panY: number, panType: string = "None") {
+        if (!this.Document.lockedPosition || this.Document.inOverlay) {
+            this.Document.panTransformType = panType;
             var scale = this.getLocalTransform().inverse().Scale;
             const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
-            const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.props.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
-            this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
-            this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+            const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
+            this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+            this.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
         }
     }
 
@@ -415,8 +415,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
             let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
 
-            this.setPan(newPanX, newPanY);
-            this.Document.panTransformType = "Ease";
+            this.setPan(newPanX, newPanY, "Ease");
             Doc.BrushDoc(this.props.Document);
             this.props.focus(this.props.Document);
             willZoom && this.setScaleToZoom(layoutdoc, scale);
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index bd1caaf1b..268255b46 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -95,7 +95,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     pause = action(() => {
         this._ele!.pause();
         this._playing = false;
-    })
+    });
 
     playFrom = (seekTimeInSeconds: number) => {
         if (this._ele) {
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index 8f68effe8..f4c7d43aa 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -36,7 +36,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         (e.button === 0 && !e.ctrlKey) && e.stopPropagation();
     }
     onPointerMove = action((e: PointerEvent) => {
-        let cdiv = this._ref.current!.parentElement;
+        let cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
         if (cdiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) {
             let bounds = cdiv.getBoundingClientRect();
             let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
@@ -65,6 +65,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         }
         e.stopPropagation();
     }
+
     render() {
         let anchorDoc = Cast(this.props.Document[this.props.fieldKey], Doc);
         let hasAnchor = anchorDoc instanceof Doc && anchorDoc.type === DocumentType.PDFANNO;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ed93aa83e..11066145b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -667,9 +667,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
-                {this.Document.links && DocListCast(this.Document.links).filter(this.isNonTemporalLink).map((d, i) =>
+                {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) =>
                     <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
-                        <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} />
+                        <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} removeDocument={undoBatch((doc: Doc) => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} layoutKey={this.linkEndpoint(d)} />
                     </div>)}
                 {!showTitle && !showCaption ?
                     this.Document.searchFields ?
-- 
cgit v1.2.3-70-g09d2


From cb7078190a06ea19cb443626b9023b93c1930e3a Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 4 Nov 2019 11:35:50 -0500
Subject: getting back in the saddle: small changes -audio play icons, link
 titling, dictation.

---
 src/client/views/nodes/AudioBox.tsx         | 1 +
 src/client/views/nodes/DocuLinkBox.tsx      | 5 ++++-
 src/client/views/nodes/FormattedTextBox.tsx | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 268255b46..d652a11d5 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -193,6 +193,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     setRef = (e: HTMLAudioElement | null) => {
         e && e.addEventListener("timeupdate", this.timecodeChanged);
+        e && e.addEventListener("ended", this.pause);
         this._ele = e;
     }
 
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index f4c7d43aa..d73407903 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -72,7 +72,10 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         let y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
         let x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
         let c = StrCast(this.props.Document.backgroundColor, "lightblue");
-        return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.Document[this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"]! as Doc).title)}
+        let anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1";
+        let timecode = this.props.Document[anchor + "Timecode"];
+        let targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : "");
+        return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle}
             ref={this._ref} style={{
                 background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
                 transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})`
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index cf55a10ba..037c92a62 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1059,7 +1059,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                         e.stopPropagation();
                     }} >
                     <FontAwesomeIcon className="formattedTExtBox-audioFont"
-                        style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"file-audio"} size="sm" />
+                        style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"microphone"} size="sm" />
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From 70a6f10779729cff4a1b46d56dd5ff84f41ed14e Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 4 Nov 2019 13:08:47 -0500
Subject: cleaned up some exceptions, runtime warnings, fixed borderRounding,
 and adding to Library->Documents

---
 src/client/views/MainView.tsx                      |  2 ++
 .../views/collections/CollectionStackingView.tsx   |  1 -
 .../views/collections/CollectionTreeView.tsx       |  6 +++---
 src/client/views/nodes/AudioBox.tsx                | 24 ++++++++++++----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  8 +-------
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 7 files changed, 21 insertions(+), 24 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index cc5c5bf2b..383300b22 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -38,6 +38,7 @@ import PDFMenu from './pdf/PDFMenu';
 import { PreviewCursor } from './PreviewCursor';
 import { Scripting } from '../util/Scripting';
 import { LinkManager } from '../util/LinkManager';
+import { AudioBox } from './nodes/AudioBox';
 
 @observer
 export class MainView extends React.Component {
@@ -136,6 +137,7 @@ export class MainView extends React.Component {
 
     globalPointerDown = action((e: PointerEvent) => {
         this.isPointerDown = true;
+        AudioBox.AudioEnabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
             ContextMenu.Instance.closeMenu();
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 7f0165ad4..37d897088 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -393,7 +393,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             let entries = Array.from(this.Sections.entries());
             sections = entries.sort(this.sortFunc);
         }
-        console.log("NUM = " + this.numGroupColumns);
         return (
             <div className="collectionStackingMasonry-cont" >
                 <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 47c355fc8..89cc69ccf 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -221,10 +221,10 @@ class TreeView extends React.Component<TreeViewProps> {
             }
             let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
             return (de.data.dropAction || de.data.userDropAction) ?
-                de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, undefined, before) || added, false)
+                de.data.droppedDocuments.reduce((added, d) => addDoc(d) || added, false)
                 : de.data.moveDocument ?
                     movedDocs.reduce((added, d) => de.data.moveDocument(d, undefined, addDoc) || added, false)
-                    : de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, undefined, before), false);
+                    : de.data.droppedDocuments.reduce((added, d) => addDoc(d), false);
         }
         return false;
     }
@@ -474,7 +474,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
                 return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
             };
-            return <TreeView
+            return !(child instanceof Doc) ? (null) : <TreeView
                 document={pair.layout}
                 dataDoc={pair.data}
                 containingCollection={containingCollection}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index d652a11d5..86bd23b67 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -37,6 +37,7 @@ const AudioDocument = makeInterface(documentSchema, audioSchema);
 @observer
 export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
+    public static Enabled = false;
 
     _linkPlayDisposer: IReactionDisposer | undefined;
     _reactionDisposer: IReactionDisposer | undefined;
@@ -98,7 +99,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     });
 
     playFrom = (seekTimeInSeconds: number) => {
-        if (this._ele) {
+        if (this._ele && AudioBox.Enabled) {
             if (seekTimeInSeconds < 0) {
                 this.pause();
             } else if (seekTimeInSeconds <= this._ele.duration) {
@@ -243,16 +244,17 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                                         la2 = l.anchor1 as Doc;
                                         linkTime = NumCast(l.anchor1Timecode);
                                     }
-                                    return !linkTime ? (null) : <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
-                                        <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
-                                            <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
-                                                parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
-                                                backgroundColor={returnTransparent} />
-                                        </div>
-                                        <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
-                                            onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
-                                            onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
-                                    </div>;
+                                    return !linkTime ? (null) :
+                                        <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+                                            <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
+                                                <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+                                                    parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+                                                    backgroundColor={returnTransparent} />
+                                            </div>
+                                            <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+                                                onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
+                                                onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+                                        </div>;
                                 })}
                                 <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
                                 {this.audio}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 58cb831f8..581e5b736 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -4,7 +4,6 @@ import { observer } from "mobx-react";
 import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { percent2frac } from "../../../Utils";
 import { Transform } from "../../util/Transform";
 import { DocComponent } from "../DocComponent";
 import "./CollectionFreeFormDocumentView.scss";
@@ -71,12 +70,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
         let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
         let br = StrCast((ld || this.props.Document).borderRounding);
-        br = !br && ruleRounding ? ruleRounding : br;
-        if (br.endsWith("%")) {
-            let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight));
-            return percent2frac(br) * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
-        }
-        return undefined;
+        return !br && ruleRounding ? ruleRounding : br;
     }
 
     @computed
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 11066145b..f593a5363 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -668,7 +668,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
                 {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) =>
-                    <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
+                    <div key={`${d[Id]}`} style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
                         <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} removeDocument={undoBatch((doc: Doc) => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} layoutKey={this.linkEndpoint(d)} />
                     </div>)}
                 {!showTitle && !showCaption ?
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 188440292..212c99f9d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -323,7 +323,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
                 >
                     <FontAwesomeIcon className="imageBox-audioFont"
-                        style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
+                        style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" />
                 </div>
                 {this.considerGooglePhotosLink()}
                 <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
-- 
cgit v1.2.3-70-g09d2

