From 22ba42b2a44828d8d6284ea36d4f34b702ed6a4b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 30 Jul 2021 08:44:42 -0400
Subject: added sidebar button to mapBox

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

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

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 8cac21927..6738d481d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1471,6 +1471,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     rootDoc={this.rootDoc}
                     layoutDoc={this.layoutDoc}
                     dataDoc={this.dataDoc}
+                    usePanelWidth={true}
                     PanelWidth={this.sidebarWidth}
                     setHeight={this.setSidebarHeight}
                     sidebarAddDocument={this.sidebarAddDocument}
-- 
cgit v1.2.3-70-g09d2


From 3c1b393732ef9dc704a2f40b103c37b3f8370ba7 Mon Sep 17 00:00:00 2001
From: Aubrey Li <Aubrey-Li>
Date: Tue, 26 Oct 2021 17:16:10 -0400
Subject: update Mapbox image rendering

---
 src/client/views/nodes/MapBox/MapBox.tsx           | 207 ++++++++++++++++-----
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +
 .../Session/agents/process_message_router.ts       |   2 +-
 3 files changed, 162 insertions(+), 51 deletions(-)

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

diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 0cf3ae326..4c0c4c0c7 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -2,11 +2,11 @@ import { Autocomplete, GoogleMap, GoogleMapProps, InfoWindow, Marker } from '@re
 import { action, computed, IReactionDisposer, observable, ObservableMap } from 'mobx';
 import { observer } from "mobx-react";
 import * as React from "react";
-import { DataSym, Doc, DocListCast, FieldsSym, WidthSym } from '../../../../fields/Doc';
+import { DataSym, Doc, DocListCast, FieldsSym, Opt, WidthSym } from '../../../../fields/Doc';
 import { documentSchema } from '../../../../fields/documentSchemas';
 import { makeInterface } from '../../../../fields/Schema';
 import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
@@ -21,6 +21,13 @@ import { identity } from 'lodash';
 import { Id } from '../../../../fields/FieldSymbols';
 import { Colors } from '../../global/globalEnums';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { TraceMobx } from '../../../../fields/util';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { InkTool } from '../../../../fields/InkField';
+import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
+import { MarqueeAnnotator } from '../../MarqueeAnnotator';
+import { Annotation } from '../../pdf/Annotation';
 
 type MapDocument = makeInterface<[typeof documentSchema]>;
 const MapDocument = makeInterface(documentSchema);
@@ -72,25 +79,34 @@ const options = {
 export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>, MapDocument>(MapDocument) {
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+    @observable private _overlayAnnoInfo: Opt<Doc>;
+    showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); }
+    public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+    private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+    @computed get inlineTextAnnotations() { return this.allMapMarkers.filter(a => a.textInlineAnnotations); }
 
     @observable private _map: google.maps.Map = null as unknown as google.maps.Map;
-    @observable private selectedPlace: MapMarker | undefined;
+    @observable private selectedPlace: MapMarker | Doc | undefined;
     @observable private markerMap: { [id: string]: google.maps.Marker } = {};
     @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter;
     @observable private zoom = 2.5;
     @observable private infoWindowOpen = false;
+    @observable private _marqueeing: number[] | undefined;
+    @observable private _isAnnotating = false;
     @observable private bounds = new window.google.maps.LatLngBounds();
     @observable private inputRef = React.createRef<HTMLInputElement>();
     @observable private searchMarkers: google.maps.Marker[] = [];
     @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
     @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
     @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); };
-    @computed get allMapMarkers() { return DocListCast(this.dataDoc[this.annotationKey]); };
-    @observable private allMarkers: Doc[] = [];
+    @computed get allMapMarkers() { return DocListCast(this.dataDoc[this.annotationKey]); }; // method to add MapMarker to allMapMarkers
     //TODO: change all markers to a filter function to change 
     @observable private toggleAddMarker = false;
 
+    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
 
     @observable _showSidebar = false;
     @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
@@ -112,7 +128,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     // iterate allMarkers to size, center, and zoom map to contain all markers
     private fitBounds = (map: google.maps.Map) => {
         console.log('map bound is:' + this.bounds);
-        this.allMarkers.map(place => {
+        this.allMapMarkers.map(place => {
             this.bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) });
             return place._markerId;
         });
@@ -185,29 +201,28 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         });
         map.panTo(position);
         const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), [], {});
-        this.allMarkers.push(mapMarker);
+        this.addDocument(mapMarker, this.annotationKey);
+        // Doc.AddDocToList(this.dataDoc, this.annotationKey, mapMarker);
     }
 
     /**
      * A function that examines allMapMarkers docs in the map node and form MapMarkers
      */
-    private fillMarkers = () => {
-        this.allMapMarkers?.forEach(doc => {
-            console.log(doc);
-            // search for if the map marker exists, else create marker
-            if (doc.lat !== undefined && doc.lng !== undefined) {
-                console.log("image found! loading into marker document...")
-                const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), [doc], {})
-                this.allMarkers.push(marker)
-            }
-        })
-    }
-
-    // TODO: things to ask & think about when designing
-    // 1. All markers are stored in allMarkers[], when adding a new marker (from a button, ideally not using drawManager),
-    // the new marker will be stored in allMarkers[]
-    // currently markerloadhandler only gets called when the map is reloaded, but we want it to be update on the GUI in real time
-    // TODO ** core issue --> real time updates ** 
+    // private fillMarkers = () => {
+    //     // console.log("allSidebarDocs:");
+    //     // console.log(this.allSidebarDocs);
+    //     this.allSidebarDocs?.forEach(doc => {
+    //         console.log(doc);
+    //         // search for if the map marker exists, else create marker
+    //         if (doc.lat !== undefined && doc.lng !== undefined) {
+    //             console.log("image found! loading into marker document...")
+    //             const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), [doc], {})
+    //             Doc.AddDocToList(this.dataDoc, this.annotationKey, marker) // add marker to annotation key
+    //         }
+    //     });
+    //     // console.log("allMarkers:")
+    //     // console.log(this.allMarkers);
+    // }
 
 
     /**
@@ -235,20 +250,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             );
         } else {
             alert("Your geolocation is not supported by browser.")
-        }
-        this.fitBounds(map);
-        console.log("content in the allMapsMarker array:");
-        console.log(this.allMapMarkers);
-        console.log("content in the sidebarDocs array:");
+        };
+        console.log("all sidebar docs during map loading is:")
         console.log(this.allSidebarDocs);
-        this.fillMarkers();
+        this.fitBounds(map);
+        // this.fillMarkers();
+
 
         // listener to addmarker event
         this._map.addListener('click', (e) => {
             console.log("add marker map status:" + this.toggleAddMarker);
             if (this.toggleAddMarker == true) {
                 this.placeMarker(e.latLng, map)
-                console.log(this.allMarkers)
+                console.log(this.allMapMarkers)
             }
         })
         // this._map.addListener(drawingManager, 'markercomplete', this.addMarker)
@@ -281,10 +295,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     }
 
     @action
-    private markerClickHandler = (e: MouseEvent, place: any) => {
+    private markerClickHandler = (e: MouseEvent, place: Doc) => {
         // set which place was clicked
         this.selectedPlace = place;
 
+        console.log("you have selected this location:");
         console.log(this.selectedPlace);
 
         // used so clicking a second marker works
@@ -303,6 +318,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
      * @returns 
      */
     sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+        console.log("print all sidebar Docs");
+        console.log(this.allSidebarDocs);
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
         const docs = doc instanceof Doc ? [doc] : doc
         docs.forEach(doc => {
@@ -311,11 +328,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 this.addDocument(marker, this.annotationKey)
             }
         }) //add to annotation list
+        console.log("sidebaraddDocument");
+        console.log(doc);
+
         return this.addDocument(doc, sidebarKey); // add to sidebar list
     }
 
     /**
-     * What does this do exactly? How to operate on sidebar?
+     * Toggle sidebar onclick the tiny comment button on the top right corner
      * @param e 
      */
     sidebarBtnDown = (e: React.PointerEvent) => {
@@ -397,16 +417,26 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         this.selectedPlace = undefined;
     }
 
-    public get SidebarKey() { return this.fieldKey + "-sidebar"; }
-
     @computed get sidebarHandle() {
+        TraceMobx();
         const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
-        //&& !this.isContentActive()
-        return (!annotated) ? (null) : <div className="mapBox-sidebar-handle" onPointerDown={this.sidebarDown}
-            style={{
-                left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`,
-                background: this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""))
-            }} />;
+        const color = !annotated ? Colors.WHITE : Colors.BLACK;
+        const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
+        return (!annotated) ? (null) :
+            <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
+                style={{
+                    left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`,
+                    backgroundColor: backgroundColor,
+                    color: color,
+                    opacity: annotated ? 1 : undefined
+                }} >
+                <FontAwesomeIcon icon={"comment-alt"} />
+            </div>;
+        // return (!annotated) ? (null) : <div className="mapBox-sidebar-handle" onPointerDown={this.sidebarDown}
+        //     style={{
+        //         left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`,
+        //         background: this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""))
+        //     }} />;
     }
 
     @action
@@ -415,6 +445,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
         this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
     }
+
     sidebarDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false);
     }
@@ -426,7 +457,29 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return false;
     }
 
-    // TODO what's the difference between savedAnnotations & allMapMarkers?
+    setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
+
+    @action
+    onMarqueeDown = (e: React.PointerEvent) => {
+        if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+            setupMoveUpEvents(this, e, action(e => {
+                MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+                this._marqueeing = [e.clientX, e.clientY];
+                return true;
+            }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+        }
+    }
+    @action finishMarquee = (x?: number, y?: number) => {
+        this._marqueeing = undefined;
+        this._isAnnotating = false;
+        x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+    }
+
+    addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+        return this.addDocument(doc, annotationKey);
+    }
+
+
     getAnchor = () => {
         const anchor =
             AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
@@ -440,12 +493,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return anchor;
     }
 
-    private saveMarkerInfo = () => {
-
-    }
     // create marker prop --> func that 
     private renderMarkers = () => {
-        return this.allMarkers.map(place => (
+        return this.allMapMarkers.map(place => (
             <Marker
                 key={place[Id]}
                 position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }}
@@ -458,7 +508,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     private renderInfoWindow = () => {
         return this.infoWindowOpen && this.selectedPlace && (
             <InfoWindow
-                anchor={this.markerMap[this.selectedPlace._markerId!]}
+                // anchor={this.markerMap[this.selectedPlace.Id!]}
                 onCloseClick={this.handleInfoWindowClose}
             >
                 <div style={{ backgroundColor: 'white', opacity: 0.75, padding: 12 }}>
@@ -485,7 +535,48 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         )
     }
 
+    panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
+    panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
+    transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
+    opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+
+    anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+
+    @computed get annotationLayer() {
+        TraceMobx();
+        return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+            {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno =>
+                <Annotation {...this.props} fieldKey={this.annotationKey} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)
+            }
+        </div>;
+
+    }
+
     render() {
+        const renderAnnotations = (docFilters?: () => string[]) =>
+            <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                renderDepth={this.props.renderDepth + 1}
+                isAnnotationOverlay={true}
+                fieldKey={this.annotationKey}
+                CollectionView={undefined}
+                setPreviewCursor={this.setPreviewCursor}
+                PanelWidth={this.panelWidth}
+                PanelHeight={this.panelHeight}
+                ScreenToLocalTransform={this.scrollXf}
+                scaling={returnOne}
+                dropAction={"alias"}
+                docFilters={docFilters || this.props.docFilters}
+                dontRenderDocuments={docFilters ? false : true}
+                select={emptyFunction}
+                ContentScaling={returnOne}
+                bringToFront={emptyFunction}
+                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                removeDocument={this.removeDocument}
+                moveDocument={this.moveDocument}
+                addDocument={this.sidebarAddDocument}
+                childPointerEvents={true}
+                pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
         return <div className="mapBox" ref={this._ref}
         // style={{ pointerEvents: this.isContentActive() ? undefined : "none" }} 
         >
@@ -497,10 +588,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 onWheel={e => e.stopPropagation()}
                 onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()}
                 style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+
+                <div style={{ mixBlendMode: "multiply" }}>
+                    {renderAnnotations(this.transparentFilter)}
+                </div>
+                {renderAnnotations(this.opaqueFilter)}
+                {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()}
+                {this.annotationLayer}
                 <GoogleMap
                     mapContainerStyle={mapContainerStyle}
                     zoom={this.zoom}
-                    // center={this.center}
                     onLoad={map => this.loadHandler(map)}
                     options={mapOptions}
                 >
@@ -513,13 +610,24 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     {this.renderMarkers()}
                     {this.renderInfoWindow()}
                 </GoogleMap>
+                {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+                    <MarqueeAnnotator rootDoc={this.rootDoc}
+                        anchorMenuClick={this.anchorMenuClick}
+                        scrollTop={0}
+                        down={this._marqueeing} scaling={returnOne}
+                        addDocument={this.addDocumentWrapper}
+                        docView={this.props.docViewPath().lastElement()}
+                        finishMarquee={this.finishMarquee}
+                        savedAnnotations={this._savedAnnotations}
+                        annotationLayer={this._annotationLayer.current}
+                        mainCont={this._mainCont.current} />}
             </div>
             {/* {/* </LoadScript > */}
             <div className="mapBox-sidebar"
                 style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                 <SidebarAnnos ref={this._sidebarRef}
                     {...this.props}
-                    fieldKey={this.annotationKey}
+                    fieldKey={this.fieldKey}
                     rootDoc={this.rootDoc}
                     layoutDoc={this.layoutDoc}
                     dataDoc={this.dataDoc}
@@ -530,7 +638,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     sidebarAddDocument={this.sidebarAddDocument}
                     moveDocument={this.moveDocument}
                     removeDocument={this.removeDocument}
-                //isContentActive={this.isContentActive}
                 />
             </div>
             <div className="mapBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar"
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 63d2c1007..0d38bd5b8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -120,6 +120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     public ProseRef?: HTMLDivElement;
     public get EditorView() { return this._editorView; }
     public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+    @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); };
 
     @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
     @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
@@ -1488,6 +1489,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
     sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+        // console.log("printting allSideBarDocs");
+        // console.log(this.allSidebarDocs);
         return this.addDocument(doc, sidebarKey);
     }
     sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
@@ -1506,6 +1509,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
         const color = !annotated ? Colors.WHITE : Colors.BLACK;
         const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
+
         return (!annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging())) ? (null) :
             <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
                 style={{
diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts
index 6cc8aa941..0745ea455 100644
--- a/src/server/DashSession/Session/agents/process_message_router.ts
+++ b/src/server/DashSession/Session/agents/process_message_router.ts
@@ -33,7 +33,7 @@ export default abstract class IPCMessageReceiver {
         }
     }
 
-    /**
+    /** 
      * Unregister all listeners at this message.
      */
     public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]);
-- 
cgit v1.2.3-70-g09d2


From 3381bbb0ef5160707513f4bbbe551ca551b64b0d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 13 Nov 2021 10:38:53 -0500
Subject: change isContentActive to a tri-state to allow turning on/off and
 default - fixes issues with videobox and others so that content can be turned
 off reliably.   added annotation overlay for treeViews for ppt like slides. 
 lots of fixes to tree view to get layout to be more robust.

---
 src/Utils.ts                                       |   2 +-
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/CurrentUserUtils.ts                |  20 +-
 src/client/views/DocComponent.tsx                  |  18 +-
 src/client/views/DocumentDecorations.scss          |   6 +-
 src/client/views/DocumentDecorations.tsx           |  27 +--
 src/client/views/MainView.tsx                      |   6 +-
 src/client/views/OverlayView.tsx                   |   2 +-
 src/client/views/Palette.tsx                       |   2 +-
 src/client/views/PropertiesView.tsx                |   2 +-
 .../views/collections/CollectionStackingView.tsx   |   2 +-
 src/client/views/collections/CollectionSubView.tsx |   7 +-
 .../views/collections/CollectionTreeView.scss      |   9 +
 .../views/collections/CollectionTreeView.tsx       | 252 ++++++++++++---------
 src/client/views/collections/CollectionView.tsx    |  12 +-
 src/client/views/collections/TabDocView.tsx        |   3 +-
 src/client/views/collections/TreeView.scss         |   7 +-
 src/client/views/collections/TreeView.tsx          |  49 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  10 +-
 .../CollectionMulticolumnView.tsx                  |   4 +-
 .../CollectionMultirowView.tsx                     |   4 +-
 .../collectionMulticolumn/MulticolumnResizer.tsx   |   2 +-
 .../collectionMulticolumn/MultirowResizer.tsx      |   2 +-
 .../collectionSchema/CollectionSchemaHeaders.tsx   |   2 +-
 .../collections/collectionSchema/SchemaTable.tsx   |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  10 +-
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/FilterBox.tsx               |   2 +-
 src/client/views/nodes/LinkBox.tsx                 |   2 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   2 +-
 src/client/views/nodes/ScreenshotBox.tsx           |   2 +-
 src/client/views/nodes/VideoBox.scss               |  10 +-
 src/client/views/nodes/VideoBox.tsx                |  11 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  18 +-
 src/mobile/AudioUpload.tsx                         |   2 +-
 src/mobile/MobileInterface.tsx                     |   2 +-
 36 files changed, 298 insertions(+), 221 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index bfb29fe8d..ca1432de2 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -448,7 +448,7 @@ export function returnEmptyDoclist() { return [] as any[]; }
 
 export let emptyPath = [];
 
-export function emptyFunction() { }
+export function emptyFunction() { return undefined; }
 
 export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index da5c8efa9..219f51a3a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -94,6 +94,7 @@ type DROPt = DAInfo | dropActionType;
 export class DocumentOptions {
     system?: BOOLt = new BoolInfo("is this a system created/owned doc");
     _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
+    allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?");
     childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
     targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? ");
     color?: string; // foreground color data doc
@@ -289,6 +290,7 @@ export class DocumentOptions {
     treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed
     treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
     treeViewTruncateTitleWidth?: number;
+    treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations
     treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy
     sidebarColor?: string;  // background color of text sidebar
     sidebarViewType?: string; // collection type of text sidebar
@@ -821,7 +823,7 @@ export namespace Docs {
         }
 
         export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
         }
 
         export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9d06ad8a3..c9bef5924 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -339,7 +339,7 @@ export class CurrentUserUtils {
         const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc);
         if (doc.templateDocs === undefined) {
             doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], {
-                title: "template layouts", _xPadding: 0, system: true,
+                title: "template layouts", _xMargin: 0, system: true,
                 dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
             }));
         }
@@ -421,7 +421,11 @@ export class CurrentUserUtils {
             ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0;
         }
         if (doc.emptySlide === undefined) {
-            const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", _autoHeight: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) });
+            const textDoc = Docs.Create.TreeDocument([], {
+                title: "Slide", _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
+                allowOverlayDrop: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true,
+                backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"])
+            });
             Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text');
             FormattedTextBox.SelectOnLoad = textDoc[Id];
             doc.emptySlide = textDoc;
@@ -811,7 +815,7 @@ export class CurrentUserUtils {
             const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
             doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
                 title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add",
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true,
                 explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
@@ -840,7 +844,7 @@ export class CurrentUserUtils {
             const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
             doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], {
                 title: "My Trails", _showTitle: "title", _height: 100,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
                 explainer: "All of the trails that you have created will appear here."
@@ -865,7 +869,7 @@ export class CurrentUserUtils {
             });
             doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], {
                 title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true,
@@ -884,7 +888,7 @@ export class CurrentUserUtils {
             const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true });
             doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], {
                 title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, childHideLinkButton: true,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
                 explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -902,7 +906,7 @@ export class CurrentUserUtils {
         if (doc.currentFilter === undefined) {
             doc.currentFilter = Docs.Create.FilterDocument({
                 title: "Unnamed Filter", _height: 150,
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
+                treeViewHideTitle: true, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true
             });
@@ -918,7 +922,7 @@ export class CurrentUserUtils {
             doc.treeViewOpen = true;
             doc.treeViewExpandedView = "fields";
             doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], {
-                treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
+                treeViewHideTitle: true, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
                 treeViewTruncateTitleWidth: 150, ignoreClick: true,
                 _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true
             })) as any as Doc;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 32c351bf5..b9772fd57 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -44,7 +44,7 @@ interface ViewBoxBaseProps {
     fieldKey: string;
     layerProvider?: (doc: Doc, assign?: boolean) => boolean;
     isSelected: (outsideReaction?: boolean) => boolean;
-    isContentActive: () => boolean;
+    isContentActive: () => boolean | undefined;
     renderDepth: number;
     rootSelected: (outsideReaction?: boolean) => boolean;
 }
@@ -65,10 +65,12 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
 
         lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
 
-        isContentActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None ||
-            (this.props.isContentActive?.() || this.props.Document.forceActive ||
-                this.props.isSelected(outsideReaction) ||
-                this.props.rootSelected(outsideReaction)) ? true : false)
+        isContentActive = (outsideReaction?: boolean) => (
+            this.props.isContentActive?.() === false ? false :
+                (CurrentUserUtils.SelectedTool !== InkTool.None ||
+                    (this.props.isContentActive?.() || this.props.Document.forceActive ||
+                        this.props.isSelected(outsideReaction) ||
+                        this.props.rootSelected(outsideReaction)) ? true : undefined))
         protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
     }
     return Component;
@@ -82,7 +84,7 @@ export interface ViewBoxAnnotatableProps {
     fieldKey: string;
     filterAddDocument?: (doc: Doc[]) => boolean;  // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
     layerProvider?: (doc: Doc, assign?: boolean) => boolean;
-    isContentActive: () => boolean;
+    isContentActive: () => boolean | undefined;
     select: (isCtrlPressed: boolean) => void;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     isSelected: (outsideReaction?: boolean) => boolean;
@@ -165,13 +167,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
         // otherwise, the document being moved must be able to be removed from its container before
         // moving it into the target.
         @action.bound
-        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean => {
+        moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
             if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
                 return true;
             }
             const first = doc instanceof Doc ? doc : doc[0];
             if (!first?._stayInCollection && addDocument !== returnFalse) {
-                return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc));
+                return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
             }
             return false;
         }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index d8ad47ecb..82dca1287 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -240,8 +240,10 @@ $linkGap: 3px;
         text-align: center;
         display: flex;
         margin-left: 5px;
-        height: 22px;
+        height: 20px;
         position: absolute;
+        border-radius: 8px;
+        background: rgba(159,159,159,0.1);
 
         .documentDecorations-titleSpan, 
         .documentDecorations-titleSpan-Dark  {
@@ -288,7 +290,7 @@ $linkGap: 3px;
     text-align: center;
     display: flex;
     margin-left: 5px;
-    height: 22px;
+    height: 20px;
     position: absolute;
     .documentDecorations-titleSpan {
       width: 100%;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 522995479..1c0b1b995 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -454,17 +454,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
             </Tooltip>);
 
         const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
-        const titleArea = this._edtingTitle ?
-            <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
-                style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }}
-                type="text" name="dynbox" autoComplete="on"
-                value={this._accumulatedTitle}
-                onBlur={e => this.titleBlur()}
-                onChange={action(e => this._accumulatedTitle = e.target.value)}
-                onKeyPress={this.titleEntered} /> :
-            <div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
-                <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
-            </div>;
+        const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} key="title" /> :
+            this._edtingTitle ?
+                <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
+                    style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }}
+                    type="text" name="dynbox" autoComplete="on"
+                    value={this._accumulatedTitle}
+                    onBlur={e => this.titleBlur()}
+                    onChange={action(e => this._accumulatedTitle = e.target.value)}
+                    onKeyPress={this.titleEntered} /> :
+                <div className="documentDecorations-title" style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
+                    <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
+                </div>;
 
         let inMainMenuPanel = false;
         for (let node = seldoc.ContentDiv; node && !inMainMenuPanel; node = node?.parentNode as any) {
@@ -498,8 +499,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
                     top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
                 }}>
                     {!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")}
-                    {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
-
+                    {titleArea}
+                    {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
                     {hideResizers ? (null) :
                         <>
                             {SelectionManager.Views().length !== 1 || hideTitle ? (null) :
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 9a885fbf8..546b0e360 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -511,7 +511,7 @@ export class MainView extends React.Component {
                     bringToFront={emptyFunction}
                     select={emptyFunction}
                     isAnyChildContentActive={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     isSelected={returnFalse}
                     docViewPath={returnEmptyDoclist}
                     moveDocument={this.moveButtonDoc}
@@ -592,7 +592,7 @@ export class MainView extends React.Component {
                     pinToPres={returnFalse}
                     ScreenToLocalTransform={Transform.Identity}
                     bringToFront={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     whenChildContentsActiveChanged={returnFalse}
                     focus={returnFalse}
                     docViewPath={returnEmptyDoclist}
@@ -669,7 +669,7 @@ export class MainView extends React.Component {
                             pinToPres={returnFalse}
                             ScreenToLocalTransform={Transform.Identity}
                             bringToFront={returnFalse}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             whenChildContentsActiveChanged={returnFalse}
                             focus={returnFalse}
                             PanelWidth={() => 500}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index af04b967a..7cf388872 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -191,7 +191,7 @@ export class OverlayView extends React.Component {
                     ScreenToLocalTransform={Transform.Identity}
                     renderDepth={1}
                     isDocumentActive={returnTrue}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     whenChildContentsActiveChanged={emptyFunction}
                     focus={DocUtils.DefaultFocus}
                     styleProvider={DefaultStyleProvider}
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 86ab881bb..529697f71 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -50,7 +50,7 @@ export default class Palette extends React.Component<PaletteProps> {
                             PanelHeight={() => window.screen.height}
                             renderDepth={0}
                             isDocumentActive={returnTrue}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             focus={emptyFunction}
                             docViewPath={returnEmptyDoclist}
                             styleProvider={returnEmptyString}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ab9022a84..1083e0075 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -295,7 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     freezeDimensions={true}
                     dontCenter={"y"}
                     isDocumentActive={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
                     NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
                     PanelWidth={panelWidth}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 648ff5087..bffaf86b1 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -226,7 +226,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
             layerProvider={this.props.layerProvider}
             docViewPath={this.props.docViewPath}
             fitWidth={this.props.childFitWidth}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             isDocumentActive={this.isContentActive}
             LayoutTemplate={this.props.childLayoutTemplate}
             LayoutTemplateString={this.props.childLayoutString}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5dffc65fc..fc1bcb8b9 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,7 +22,6 @@ import ReactLoading from 'react-loading';
 
 export interface SubCollectionViewProps extends CollectionViewProps {
     CollectionView: Opt<CollectionView>;
-    SetSubView?: (subView: any) => void;
     isAnyChildContentActive: () => boolean;
 }
 
@@ -49,10 +48,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
             this.createDashEventsTarget(ele);
         }
 
-        componentDidMount() {
-            this.props.SetSubView?.(this);
-        }
-
         componentWillUnmount() {
             this.gestureDisposer?.();
             this._multiTouchDisposer?.();
@@ -220,7 +215,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     if (movedDocs.length) {
-                        const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay ||
+                        const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || (!this.props.isAnnotationOverlay || this.props.Document.allowOverlayDrop) ||
                             Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
                         added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
                     } else {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index d370d21ab..b664d9d82 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -71,6 +71,15 @@
     display: none;
 }
 
+.collectionTreeView-titleBar {
+    display: inline-block;
+    width: 100%;
+    height: max-content;
+    .contentFittingDocumentView {
+        display: block;  // makes titleBar take up full width of the treeView (flex doesn't for some reason)
+    }
+}
+
 .collectionTreeView-keyHeader:hover {
     background: #797777;
     cursor: pointer;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3852987b9..6fb18d4c2 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
@@ -8,13 +7,14 @@ import { Document, listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from "../../util/DragManager";
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
@@ -22,11 +22,11 @@ import { EditableView } from "../EditableView";
 import { DocumentView } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView } from "./CollectionSubView";
 import "./CollectionTreeView.scss";
 import { TreeView } from "./TreeView";
 import React = require("react");
-import { Transform } from '../../util/Transform';
 const _global = (window /* browser */ || global /* node */) as any;
 
 export type collectionTreeViewProps = {
@@ -41,10 +41,13 @@ export type collectionTreeViewProps = {
 
 @observer
 export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) {
-    private treedropDisposer?: DragManager.DragDropDisposer;
+    private _treedropDisposer?: DragManager.DragDropDisposer;
     private _mainEle?: HTMLDivElement;
+    private _titleRef?: HTMLDivElement | HTMLInputElement | null;
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    MainEle = () => this._mainEle;
+    private _isDisposing = false; // notes that instance is in process of being disposed
+    private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes
+    private observer: any; // observer for mnonitoring tree view items.
 
     @computed get doc() { return this.props.Document; }
     @computed get dataDoc() { return this.props.DataDoc || this.doc; }
@@ -54,6 +57,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
     @computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
     @computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
 
+    @observable _explainerHeight = 0; // height of the description of the tree view
+
+    MainEle = () => this._mainEle;
+
     // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
     @observable _isAnyChildContentActive = false;
     whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
@@ -62,11 +69,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
             this.props.isSelected(outsideReaction) || this._isAnyChildContentActive ||
             this.props.rootSelected(outsideReaction)) ? true : false)
 
-    isDisposing = false;
     componentWillUnmount() {
-        this.isDisposing = true;
+        this._isDisposing = true;
         super.componentWillUnmount();
-        this.treedropDisposer?.();
+        this._treedropDisposer?.();
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
@@ -76,13 +82,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
             { fireImmediately: true });
     }
 
-    refList: Set<any> = new Set();
-    observer: any;
     computeHeight = () => {
-        if (this.isDisposing) return;
-        const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot());
-        this.layoutDoc._autoHeightMargins = bodyHeight;
-        this.props.setHeight(this.documentTitleHeight() + bodyHeight);
+        if (!this._isDisposing) {
+            const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", ""));
+            const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot());
+            this.layoutDoc._autoHeightMargins = bodyHeight;
+            this.props.setHeight(bodyHeight + titleHeight);
+        }
     }
     unobserveHeight = (ref: any) => {
         this.refList.delete(ref);
@@ -101,8 +107,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         }
     }
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
-        this.treedropDisposer?.();
-        if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
+        this._treedropDisposer?.();
+        if (this._mainEle = ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
     }
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
@@ -165,60 +171,54 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
     }
 
-    editableTitle = (childDocs: Doc[]) => {
-        return !this.dataDoc ? (null) :
-            <EditableView
-                contents={this.dataDoc.title}
-                display={"block"}
-                maxHeight={72}
-                height={"auto"}
-                GetValue={() => StrCast(this.dataDoc.title)}
-                SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
-                    if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(childDocs);
-                    this.dataDoc.title = value;
-                    return true;
-                })} />;
+    get editableTitle() {
+        return <EditableView
+            contents={this.dataDoc.title}
+            display={"block"}
+            maxHeight={72}
+            height={"auto"}
+            GetValue={() => StrCast(this.dataDoc.title)}
+            SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
+                if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren);
+                this.dataDoc.title = value;
+                return true;
+            })} />;
     }
 
 
-    documentTitle = (childDocs: Doc[]) => {
-        return <div style={{ display: "inline-block", width: "100%", height: this.documentTitleHeight() }} key={this.doc[Id]}
-            onKeyDown={e => {
-                e.stopPropagation();
-                e.key === "Enter" && this.makeTextCollection(childDocs);
-            }}>
-            <DocumentView
-                Document={this.doc}
-                DataDoc={undefined}
-                LayoutTemplateString={FormattedTextBox.LayoutString("text")}
-                renderDepth={this.props.renderDepth + 1}
-                isContentActive={this.isContentActive}
-                isDocumentActive={this.isContentActive}
-                rootSelected={returnTrue}
-                docViewPath={this.props.docViewPath}
-                styleProvider={this.props.styleProvider}
-                layerProvider={this.props.layerProvider}
-                PanelWidth={this.documentTitleWidth}
-                PanelHeight={this.documentTitleHeight}
-                NativeWidth={this.documentTitleWidth}
-                NativeHeight={this.documentTitleHeight}
-                focus={this.props.focus}
-                treeViewDoc={this.props.Document}
-                ScreenToLocalTransform={this.titleTransform}
-                docFilters={returnEmptyFilter}
-                docRangeFilters={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-                ContainingCollectionDoc={this.doc}
-                ContainingCollectionView={this.props.CollectionView}
-                addDocument={this.props.addDocument}
-                moveDocument={returnFalse}
-                removeDocument={returnFalse}
-                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                addDocTab={this.props.addDocTab}
-                pinToPres={this.props.pinToPres}
-                bringToFront={returnFalse}
-            />
-        </div>;
+    get documentTitle() {
+        return <DocumentView
+            Document={this.doc}
+            DataDoc={undefined}
+            LayoutTemplateString={FormattedTextBox.LayoutString("text")}
+            renderDepth={this.props.renderDepth + 1}
+            isContentActive={this.isContentActive}
+            isDocumentActive={this.isContentActive}
+            rootSelected={returnTrue}
+            forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
+            docViewPath={this.props.docViewPath}
+            styleProvider={this.props.styleProvider}
+            layerProvider={this.props.layerProvider}
+            PanelWidth={this.documentTitleWidth}
+            PanelHeight={this.documentTitleHeight}
+            NativeWidth={this.documentTitleWidth}
+            NativeHeight={this.documentTitleHeight}
+            focus={this.props.focus}
+            treeViewDoc={this.props.Document}
+            ScreenToLocalTransform={this.titleTransform}
+            docFilters={returnEmptyFilter}
+            docRangeFilters={returnEmptyFilter}
+            searchFilterDocs={returnEmptyDoclist}
+            ContainingCollectionDoc={this.doc}
+            ContainingCollectionView={this.props.CollectionView}
+            addDocument={this.props.addDocument}
+            moveDocument={returnFalse}
+            removeDocument={returnFalse}
+            whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+            addDocTab={this.props.addDocTab}
+            pinToPres={this.props.pinToPres}
+            bringToFront={returnFalse}
+        />;
     }
     childContextMenuItems = () => {
         const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -263,21 +263,31 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         );
     }
     @computed get titleBar() {
-        const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
-        return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren);
+        return this.dataDoc === null ? (null) :
+            <div className="collectionTreeView-titleBar" key={this.doc[Id]}
+                style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingRight: this.marginX(), paddingTop: this.marginTop() } : {}}
+                ref={r => this._titleRef = r}
+                onKeyDown={e => {
+                    if (this.outlineMode) {
+                        e.stopPropagation();
+                        e.key === "Enter" && this.makeTextCollection(this.treeChildren);
+                    }
+                }}>
+                {this.outlineMode ? this.documentTitle : this.editableTitle}
+            </div>;
+    }
+
+    @computed get noviceExplainer() {
+        return !Doc.UserDoc().noviceMode || !this.rootDoc.explainer ? (null) :
+            <div className="documentExplanation"> {this.rootDoc.explainer} </div>;
     }
 
     return35 = () => 35;
     @computed get buttonMenu() {
-        const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+        const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
         // To create a multibutton menu add a CollectionLinearView
-        if (menuDoc) {
-
-            const width: number = NumCast(menuDoc._width, 30);
-            const height: number = NumCast(menuDoc._height, 30);
-            console.log(menuDoc.title, width, height);
-            return (<div className="buttonMenu-docBtn"
-                style={{ width: width, height: height }}>
+        return !menuDoc ? null :
+            (<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
                 <DocumentView
                     Document={menuDoc}
                     DataDoc={menuDoc}
@@ -306,11 +316,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
                     ContainingCollectionDoc={undefined}
                 />
             </div>);
-        }
     }
 
-    @observable _explainerHeight: number = 0;
-
     @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); }
     @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); }
 
@@ -321,47 +328,82 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
         const wscale = nw ? this.props.PanelWidth() / nw : 1;
         return wscale < hscale ? wscale : hscale;
     }
-    paddingX = () => NumCast(this.doc._xPadding, 15);
-    paddingTop = () => NumCast(this.doc._yPadding, 20);
-    paddingBot = () => NumCast(this.doc._yPadding, 20);
+    marginX = () => NumCast(this.doc._xMargin);
+    marginTop = () => NumCast(this.doc._yMargin);
+    marginBot = () => NumCast(this.doc._yMargin);
     documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
     documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
     titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20));
     truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
     onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
-    panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1);
-    render() {
-        TraceMobx();
+    panelWidth = () => (this.props.PanelWidth() - 2 * this.marginX()) * (this.props.scaling?.() || 1);
+
+    addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+    remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+    moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
+        this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false
+
+    contentFunc = () => {
         const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
         const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined;
-        const buttonMenu = this.rootDoc.buttonMenu;
-        const noviceExplainer = this.rootDoc.explainer;
-
-        return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
-            <>
-                {this.titleBar}
+        const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar;
+        return [
+            <div className="collectionTreeView-contents" key="tree" style={{
+                ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+                overflow: "auto",
+                height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+            }} >
+                {titleBar}
                 <div className="collectionTreeView-container"
-                    style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}}
+                    style={{
+                        transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
+                        paddingLeft: `${this.marginX()}px`,
+                        height: "max-content",
+                        width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
+                    }}
                     onContextMenu={this.onContextMenu}>
-                    {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
-                        {buttonMenu ? this.buttonMenu : null}
-                        {Doc.UserDoc().noviceMode && noviceExplainer ?
-                            <div className="documentExplanation">
-                                {noviceExplainer}
-                            </div>
-                            : null
-                        }
-                    </div> : null}
+                    {!this.buttonMenu && !this.noviceExplainer ? (null) :
+                        <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
+                            {this.buttonMenu}
+                            {this.noviceExplainer}
+                        </div>
+                    }
                     <div className="collectionTreeView-dropTarget"
-                        style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }}
+                        style={{
+                            background: background(),
+                            height: `calc(100% - ${this._explainerHeight}px)`,
+                            pointerEvents: pointerEvents()
+                        }}
                         onWheel={e => e.stopPropagation()}
                         onDrop={this.onTreeDrop}
-                        ref={this.createTreeDropTarget}>
+                        ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
                         <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} >
                             {this.treeViewElements}
                         </ul>
                     </div >
                 </div>
-            </>;
+            </div>
+        ];
+    }
+    render() {
+        TraceMobx();
+
+        return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
+            this.doc.treeViewHasOverlay ?
+                <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+                    isAnnotationOverlay={true}
+                    isAnnotationOverlayScrollable={true}
+                    childDocumentsActive={this.props.isDocumentActive}
+                    fieldKey={this.props.fieldKey + "-annotations"}
+                    dropAction={"move"}
+                    select={emptyFunction}
+                    addDocument={this.addAnnotationDocument}
+                    removeDocument={this.remAnnotationDocument}
+                    moveDocument={this.moveAnnotationDocument}
+                    bringToFront={emptyFunction}
+                    renderDepth={this.props.renderDepth + 1} >
+                    {this.contentFunc}
+                </CollectionFreeFormView> :
+                this.contentFunc();
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 38e027fb3..8e84b59de 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -63,6 +63,7 @@ export enum CollectionViewType {
 }
 export interface CollectionViewProps extends FieldViewProps {
     isAnnotationOverlay?: boolean;  // is the collection an annotation overlay (eg an overlay on an image/video/etc)
+    isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
     layoutEngine?: () => string;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
 
@@ -125,8 +126,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     }
 
     screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth());
-    private SubView = (type: CollectionViewType, props: SubCollectionViewProps) => {
+    private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
         TraceMobx();
+        if (type === undefined) return null;
         switch (type) {
             default:
             case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
@@ -246,17 +248,13 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
     @computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); }
 
-
-    @observable _subView: any = undefined;
-
     isContentActive = (outsideReaction?: boolean) => {
-        return this.props.isContentActive() ? true : false;
+        return this.props.isContentActive();
     }
     render() {
         TraceMobx();
         const props: SubCollectionViewProps = {
             ...this.props,
-            SetSubView: action((subView: any) => this._subView = subView),
             addDocument: this.addDocument,
             moveDocument: this.moveDocument,
             removeDocument: this.removeDocument,
@@ -273,7 +271,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
         return (<div className={"collectionView"} onContextMenu={this.onContextMenu}
             style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}>
             {this.showIsTagged()}
-            {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+            {this.renderSubView(this.collectionViewType, props)}
         </div>);
     }
 }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index eb95bb913..5ba019698 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -476,7 +476,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
             <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
                 <CollectionFreeFormView
                     Document={this.props.document}
-                    SetSubView={() => this}
                     CollectionView={undefined}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined}
@@ -484,7 +483,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
                     childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
                     noOverlay={true} // don't render overlay Docs since they won't scale
                     setHeight={returnFalse}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     isAnyChildContentActive={returnFalse}
                     select={emptyFunction}
                     dropAction={undefined}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 1ebc5873e..2e33d3564 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,14 +53,11 @@
     }
 }
 
+.treeView-container-outline-active
 .treeView-container-active {
     z-index: 100;
     position: relative;
-
-    .formattedTextbox-sidebar {
-        background-color: #ffff001f !important;
-        height: 500px !important;
-    }
+    pointer-events: all;
 }
 
 .treeView-openRight {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 7f2128230..a266c301f 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -81,7 +81,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     static _openLevelScript: Opt<ScriptField | undefined>;
     private _header: React.RefObject<HTMLDivElement> = React.createRef();
     private _tref = React.createRef<HTMLDivElement>();
-    private _docRef: Opt<DocumentView>;
+    @observable _docRef: Opt<DocumentView>;
     private _selDisposer: Opt<IReactionDisposer>;
     private _editTitleScript: (() => ScriptField) | undefined;
     private _openScript: (() => ScriptField) | undefined;
@@ -116,7 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> {
     @computed get childLinks() { return this.childDocList("links"); }
     @computed get childAliases() { return this.childDocList("aliases"); }
     @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); }
-    @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
+    @computed get selected() { return SelectionManager.IsSelected(this._docRef); }
+    //      SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
 
     childDocList(field: string) {
         const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
@@ -125,7 +126,12 @@ export class TreeView extends React.Component<TreeViewProps> {
             DocListCastOrNull(this.doc[field]); // otherwise use the document's data field
     }
     @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
-        return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc);
+        if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
+            if (this.props.removeDoc?.(doc) === true) {
+                return addDoc(doc);
+            }
+        }
+        return false;
     }
     @undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
         this.props.treeView.props.select(false);
@@ -141,8 +147,10 @@ export class TreeView extends React.Component<TreeViewProps> {
             this._editTitle = false;
         }
         else if (docView.isSelected()) {
+            const doc = docView.Document;
+            SelectionManager.SelectSchemaViewDoc(doc);
             this._editTitle = true;
-            this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined));
+            this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined));
         } else {
             docView.select(false);
         }
@@ -222,7 +230,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     public static makeTextBullet() {
         const bullet = Docs.Create.TextDocument("-text-", {
             layout: CollectionView.LayoutString("data"),
-            title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform,
+            title: "-title-",
             treeViewExpandedViewLock: true, treeViewExpandedView: "data",
             _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline",
             x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10
@@ -266,23 +274,25 @@ export class TreeView extends React.Component<TreeViewProps> {
             e.stopPropagation();
         }
         const docDragData = de.complete.docDragData;
-        if (docDragData) {
-            e.stopPropagation();
+        if (docDragData && pt[0] < rect.left + rect.width) {
             if (docDragData.draggedDocuments[0] === this.doc) return true;
-            this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document);
+            if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
+                e.stopPropagation();
+            }
         }
     }
 
     dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
         const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
-        const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd;
+        const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd;
         const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
         const addDoc = !inside ? parentAddDoc :
             (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
         const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument;
         if (canAdd) {
-            UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
+            return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
         }
+        return false;
     }
 
     refTransform = (ref: HTMLDivElement | undefined | null) => {
@@ -432,7 +442,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 </div>
             </ul>;
         }
-        return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout"
+        return <ul onPointerDown={e => { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}</ul>; // "layout"
     }
 
     get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); }
@@ -581,6 +591,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
     titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth()));
 
+    return18 = () => 18;
     /**
      * Renders the EditableView title element for placement into the tree.
      */
@@ -636,10 +647,10 @@ export class TreeView extends React.Component<TreeViewProps> {
                 moveDocument={this.move}
                 removeDocument={this.props.removeDoc}
                 ScreenToLocalTransform={this.getTransform}
-                NativeHeight={() => 18}
+                NativeHeight={this.return18}
                 NativeWidth={this.titleWidth}
                 PanelWidth={this.titleWidth}
-                PanelHeight={() => 18}
+                PanelHeight={this.return18}
                 contextMenuItems={this.contextMenuItems}
                 renderDepth={1}
                 isContentActive={this.props.isContentActive}
@@ -679,6 +690,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
         return <>
             <div className={`treeView-header` + (editing ? "-editing" : "")} key="titleheader"
+                style={{ width: "max-content" }}
                 ref={this._header}
                 onClick={this.ignoreEvent}
                 onPointerDown={this.ignoreEvent}
@@ -691,7 +703,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
 
 
-    renderEmbeddedDocument = (asText: boolean) => {
+    renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
         const layout = StrCast(Doc.LayoutField(this.layoutDoc));
         const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
         const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
@@ -704,8 +716,8 @@ export class TreeView extends React.Component<TreeViewProps> {
             NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined}
             NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined}
             LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined}
-            isContentActive={asText ? this.props.isContentActive : returnFalse}
-            isDocumentActive={asText ? this.props.isContentActive : returnFalse}
+            isContentActive={isActive}
+            isDocumentActive={isActive}
             styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
             hideTitle={asText}
             fitContentsToDoc={returnTrue}
@@ -749,7 +761,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     @computed get renderDocumentAsHeader() {
         return <>
             {this.renderBullet}
-            {this.renderEmbeddedDocument(true)}
+            {this.renderEmbeddedDocument(true, this.props.isContentActive)}
         </>;
     }
 
@@ -776,13 +788,12 @@ export class TreeView extends React.Component<TreeViewProps> {
         return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
             <div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`}
                 ref={this.createTreeDropTarget}
-
                 onDrop={this.onTreeDrop}
                 //onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
                 onKeyDown={this.onKeyDown}>
                 <li className="collection-child">
                     {hideTitle && this.doc.type !== DocumentType.RTF ?
-                        this.renderEmbeddedDocument(false) :
+                        this.renderEmbeddedDocument(false, returnFalse) :
                         this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
                 </li>
             </div>;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 37caf508f..190d0e76a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -165,7 +165,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
     panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
     zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1));
-    contentTransform = () => `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+    contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
     getTransform = () => this.cachedGetTransform.copy();
     getLocalTransform = () => this.cachedGetLocalTransform.copy();
     getContainerTransform = () => this.cachedGetContainerTransform.copy();
@@ -802,7 +802,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         else if (this.props.isContentActive(true) && !this.Document._isGroup) {
             e.stopPropagation();
             e.preventDefault();
-            this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+            !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
         }
     }
 
@@ -1015,7 +1015,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             docFilters={this.childDocFilters}
             docRangeFilters={this.childDocRangeFilters}
             searchFilterDocs={this.searchFilterDocs}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
             focus={this.focusDocument}
             addDocTab={this.addDocTab}
@@ -1034,7 +1034,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             bringToFront={this.bringToFront}
             showTitle={this.props.childShowTitle}
             dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
-            pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
+            pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" :
                 (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents}
             jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
         //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
@@ -1474,6 +1474,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
                 <CollectionFreeFormViewPannableContents
                     isAnnotationOverlay={this.isAnnotationOverlay}
+                    isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
                     transform={this.contentTransform}
                     zoomScaling={this.zoomScaling}
                     presPaths={BoolCast(this.Document.presPathView)}
@@ -1582,6 +1583,7 @@ interface CollectionFreeFormViewPannableContentsProps {
     progressivize?: boolean;
     presPinView?: boolean;
     isAnnotationOverlay: boolean | undefined;
+    isAnnotationOverlayScrollable: boolean | undefined;
 }
 
 @observer
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 65c345547..ec1cbadd5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
 import { List } from '../../../../fields/List';
 import { makeInterface } from '../../../../fields/Schema';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
@@ -228,7 +228,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
             LayoutTemplateString={this.props.childLayoutString}
             freezeDimensions={this.props.childFreezeDimensions}
             renderDepth={this.props.renderDepth + 1}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             PanelWidth={width}
             PanelHeight={height}
             rootSelected={this.rootSelected}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 30836854a..a2d51e2e7 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
 import { List } from '../../../../fields/List';
 import { makeInterface } from '../../../../fields/Schema';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
@@ -237,7 +237,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
             ScreenToLocalTransform={dxf}
             focus={this.props.focus}
             docFilters={this.childDocFilters}
-            isContentActive={returnFalse}
+            isContentActive={emptyFunction}
             docRangeFilters={this.childDocRangeFilters}
             searchFilterDocs={this.searchFilterDocs}
             ContainingCollectionDoc={this.props.CollectionView?.props.Document}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 9fe18d118..273e609ca 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProp } from "../../StyleProvider";
 interface ResizerProps {
     width: number;
     styleProvider?: StyleProviderFunc;
-    isContentActive?: () => boolean;
+    isContentActive?: () => boolean | undefined;
     columnUnitLength(): number | undefined;
     toLeft?: Doc;
     toRight?: Doc;
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 5478bf709..006ef4df6 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProviderFunc } from "../../nodes/DocumentView";
 interface ResizerProps {
     height: number;
     styleProvider?: StyleProviderFunc;
-    isContentActive?: () => boolean;
+    isContentActive?: () => boolean | undefined;
     columnUnitLength(): number | undefined;
     toTop?: Doc;
     toBottom?: Doc;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index 1306b79cb..dc35b5749 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -225,7 +225,7 @@ export interface KeysDropdownProps {
     fieldKey: string;
     ContainingCollectionDoc: Doc | undefined;
     ContainingCollectionView: Opt<CollectionView>;
-    active?: (outsideReaction?: boolean) => boolean;
+    active?: (outsideReaction?: boolean) => boolean | undefined;
     openHeader: (column: any, screenx: number, screeny: number) => void;
     col: SchemaHeaderField;
     icon: IconProp;
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index bc5a9559f..2219345f6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -68,7 +68,7 @@ export interface SchemaTableProps {
     addDocument?: (document: Doc | Doc[]) => boolean;
     moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
     ScreenToLocalTransform: () => Transform;
-    active: (outsideReaction: boolean | undefined) => boolean;
+    active: (outsideReaction: boolean | undefined) => boolean | undefined;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
     addDocTab: (document: Doc, where: string) => boolean;
     pinToPres: (document: Doc) => void;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6c058ff97..c8a32a911 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -138,6 +138,7 @@ export interface DocumentViewSharedProps {
     hideLinkButton?: boolean;
     hideCaptions?: boolean;
     ignoreAutoHeight?: boolean;
+    forceAutoHeight?: boolean;
     disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
     pointerEvents?: string;
     scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
@@ -224,7 +225,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     componentWillUnmount() { this.cleanupHandlers(true); }
     componentDidMount() { this.setupHandlers(); }
-    componentDidUpdate() { this.setupHandlers(); }
+    //componentDidUpdate() { this.setupHandlers(); }
     setupHandlers() {
         this.cleanupHandlers(false);
         if (this._mainCont.current) {
@@ -825,13 +826,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     }
     setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
     isContentActive = (outsideReaction?: boolean) => {
-        return CurrentUserUtils.SelectedTool !== InkTool.None ||
+        return this.props.isContentActive() === false ? false : (
+            CurrentUserUtils.SelectedTool !== InkTool.None ||
             SnappingManager.GetIsDragging() ||
             this.props.rootSelected() ||
             this.props.Document.forceActive ||
             this.props.isSelected(outsideReaction) ||
             this._componentView?.isAnyChildContentActive?.() ||
-            this.props.isContentActive() ? true : false;
+            this.props.isContentActive()) ? true : undefined;
     }
     @computed get contents() {
         TraceMobx();
@@ -1256,7 +1258,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         position: this.props.Document.isInkMask ? "absolute" : undefined,
                         transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
                         width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
-                        height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
+                        height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
                             `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
                     }}>
                     <DocumentViewInternal {...this.props}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ee81e106a..943b9f153 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,7 +18,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
     scrollOverflow?: boolean; // bcz: would like to think this can be avoided -- need to look at further
 
     select: (isCtrlPressed: boolean) => void;
-    isContentActive: (outsideReaction?: boolean) => boolean;
+    isContentActive: (outsideReaction?: boolean) => boolean | undefined;
     isDocumentActive?: () => boolean;
     isSelected: (outsideReaction?: boolean) => boolean;
     scaling?: () => number;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 2041c7399..fb8e89da9 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -225,7 +225,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
                     treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true
                 });
                 Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
-                newFacet._textBoxPadding = 4;
+                newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4;
                 const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`;
                 newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" });
             } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index b82d16677..879a63248 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -15,7 +15,7 @@ const LinkDocument = makeInterface(documentSchema);
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>(LinkDocument) {
     public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
-    isContentActiveFunc = () => this.isContentActive() ? true : false;
+    isContentActiveFunc = () => this.isContentActive();
     render() {
         if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
         return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 424083dac..2e29c0656 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -166,7 +166,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                             docViewPath={returnEmptyDoclist}
                             ScreenToLocalTransform={Transform.Identity}
                             isDocumentActive={returnFalse}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             addDocument={returnFalse}
                             removeDocument={returnFalse}
                             addDocTab={returnFalse}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 7ad96bf05..0c631e5f9 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -313,7 +313,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                             PanelHeight={this.formattedPanelHeight}
                             isAnnotationOverlay={true}
                             select={emptyFunction}
-                            isContentActive={returnFalse}
+                            isContentActive={emptyFunction}
                             scaling={returnOne}
                             xPadding={25}
                             yPadding={10}
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 4871599b8..f0d7bd2f3 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -48,10 +48,18 @@
     width: 100%;
     z-index: -1; // 0;  // logically this should be 0 (or unset) which would give us transparent brush strokes over videos.  However, this makes Chrome crawl to a halt
     position: absolute;
+    video {
+        width: auto;
+        height: 100%; 
+        display: flex; 
+        margin: auto;
+    }
 }
 
 .videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
-    height: Auto; 
+    width: 100%;
+    height: 100%; 
+    left: 0px;
 }
 
 .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen  {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 440ccf638..615d595c0 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -307,12 +307,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get content() {
         const field = Cast(this.dataDoc[this.fieldKey], VideoField);
         const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
-        const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+        const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div key="loading">Loading</div> :
-            <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}>
-                <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
+            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
+                <div className={classname}>
                     <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
-                        style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
                         onCanPlay={this.videoLoad}
                         controls={VideoBox._nativeControls}
                         onPlay={() => this.Play()}
@@ -457,11 +456,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
         this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
-        const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+        const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
         const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0)));
         return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
             onPointerLeave={this.updateTimecode}
-            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+            onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
             src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
     }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index aa53f751d..1170528df 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -123,7 +123,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
 
     @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
     @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
-    @computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; }
+    @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; }
     @computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
     @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); }
     @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); }
@@ -1565,7 +1565,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     setHeight={this.setSidebarHeight}
                     fitContentsToDoc={this.fitToBox}
                     noSidebar={true}
-                    fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : this.SidebarKey} />;
+                    fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />;
         };
         return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")}
             style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
@@ -1581,10 +1581,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false);
         if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
         const minimal = this.props.ignoreAutoHeight;
-        const margins = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
-        const selPad = Math.min(margins, 10);
-        const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0);
-        const selPaddingClass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : "";
+        const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
+        const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
+        const selPad = ((selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0);
+        const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? "-selected" : "";
         const styleFromString = this.styleFromLayoutString(scale);   // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._headerHeight}px' >
         return (styleFromString?.height === "0px" ? (null) :
             <div className="formattedTextBox-cont"
@@ -1628,7 +1628,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                         onScroll={this.onScroll} onDrop={this.ondrop} >
                         <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget}
                             style={{
-                                padding: StrCast(this.layoutDoc._textBoxPadding, `${padding}px`),
+                                padding: StrCast(this.layoutDoc._textBoxPadding),
+                                paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+                                paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+                                paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
+                                paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
                                 pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? "none" : undefined) : undefined
                             }}
                         />
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index 71ddda866..88221732e 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -94,7 +94,7 @@ export class AudioUpload extends React.Component {
                         PanelHeight={() => 400}
                         renderDepth={0}
                         isDocumentActive={returnTrue}
-                        isContentActive={returnFalse}
+                        isContentActive={emptyFunction}
                         focus={emptyFunction}
                         layerProvider={undefined}
                         styleProvider={() => "rgba(0,0,0,0)"}
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 652804126..d732a6e2f 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -211,7 +211,7 @@ export class MobileInterface extends React.Component {
                     PanelHeight={this.returnHeight}
                     renderDepth={0}
                     isDocumentActive={returnTrue}
-                    isContentActive={returnFalse}
+                    isContentActive={emptyFunction}
                     focus={DocUtils.DefaultFocus}
                     styleProvider={this.whitebackground}
                     layerProvider={undefined}
-- 
cgit v1.2.3-70-g09d2


From 4ee47eae8735adf4c543c0de4859a09dee10cbf0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 15 Nov 2021 12:25:31 -0500
Subject: fixed initial fontsize for treeView slides.  made fontsize use
 styleprovider.  fixed caption width to cover the whole document by fixing
 overflow-y setting

---
 src/client/views/StyleProvider.tsx                        | 4 +++-
 src/client/views/nodes/DocumentView.scss                  | 2 +-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 4 ++--
 3 files changed, 6 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3f120d385..8d8630907 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -46,6 +46,7 @@ export enum StyleProp {
     JitterRotation = "jitterRotation",    // whether documents should be randomly rotated
     BorderPath = "customBorder",          // border path for document view
     FontSize = "fontSize",                // size of text font
+    FontFamily = "fontFamily",                // size of text font
 }
 
 function darkScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark; }
@@ -91,7 +92,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? "lightgrey" : "dimgrey";
         case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
         case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
-        case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"]);
+        case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
+        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily)));
         case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
             StrCast(doc._showTitle,
                 props?.showTitle?.() ||
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 1ec7bf72a..9fcd45e72 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -175,7 +175,7 @@
             position: absolute;
             bottom: 0;
             width: 100%;
-            overflow-y: scroll;
+            overflow-y: auto;
             transform-origin: bottom left;
             opacity: 0.1;
             transition: opacity 0.5s;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1170528df..d9a5f2b48 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1174,8 +1174,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             ...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
             ...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []),
             ...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []),
-            ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []),
-            ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []),
+            ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
+            ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
             ...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])];
         }
     }
-- 
cgit v1.2.3-70-g09d2


From 80b27ef548936dbddaecc62f1068fff12546bf32 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 16 Nov 2021 20:36:25 -0500
Subject: added textboxes inside closed ink strokes

---
 package-lock.json                                  |  6 ++---
 src/client/views/InkStroke.scss                    | 31 ++++++++++++++--------
 src/client/views/InkingStroke.tsx                  | 30 ++++++++++++++++++---
 .../views/nodes/formattedText/FormattedTextBox.tsx | 14 +++++++---
 4 files changed, 59 insertions(+), 22 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index d03f8af6b..128d6fba8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7709,14 +7709,14 @@
             "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz",
             "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=",
             "requires": {
-                "image-size": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
                 "readable-stream": "^1.0.33",
                 "tryit": "^1.0.1"
             },
             "dependencies": {
                 "image-size": {
-                    "version": "git+ssh://git@github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1",
-                    "from": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
+                    "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1",
+                    "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1"
                 },
                 "isarray": {
                     "version": "0.0.1",
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 55e06c6ca..2127826b4 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -13,16 +13,25 @@
   }
 }
 
-.inkStroke {
-  mix-blend-mode: multiply;
-  stroke-linejoin: round;
-  stroke-linecap: round;
-  overflow: visible !important;
-  transform-origin: top left;
-  width: 100%;
-  height: 100%;
+.inkStroke-wrapper {
+    .inkStroke {
+        mix-blend-mode: multiply;
+        stroke-linejoin: round;
+        stroke-linecap: round;
+        overflow: visible !important;
+        transform-origin: top left;
+        width: 100%;
+        height: 100%;
 
-  svg:not(:root) {
-    overflow: visible !important;
-  }
+        svg:not(:root) {
+            overflow: visible !important;
+        }
+    }
+
+    .inkStroke-text {
+        position: absolute;
+        &:hover {
+            background: #9f9f9f0a;
+        }
+    }
 }
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ecb46a5b3..95dad5c6e 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,13 +1,13 @@
 import React = require("react");
 import { action, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
+import { Doc, HeightSym, WidthSym } from "../../fields/Doc";
 import { documentSchema } from "../../fields/documentSchemas";
 import { InkData, InkField, InkTool } from "../../fields/InkField";
 import { makeInterface } from "../../fields/Schema";
 import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
 import { TraceMobx } from "../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils";
+import { emptyFunction, returnFalse, setupMoveUpEvents, OmitKeys } from "../../Utils";
 import { CognitiveServices } from "../cognitive_services/CognitiveServices";
 import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { InteractionUtils } from "../util/InteractionUtils";
@@ -22,6 +22,7 @@ import { InkTangentHandles } from "./InkTangentHandles";
 import { FieldView, FieldViewProps } from "./nodes/FieldView";
 import Color = require("color");
 import { Transform } from "../util/Transform";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
 
 type InkDocument = makeInterface<[typeof documentSchema]>;
 const InkDocument = makeInterface(documentSchema);
@@ -252,9 +253,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false);
         // Set of points rendered upon the ink that can be added if a user clicks on one.
 
-        return (
+        return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
             <svg className="inkStroke"
                 style={{
+                    width: "100%",
+                    height: "100%",
                     pointerEvents: "none",
                     transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                     mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
@@ -275,7 +278,26 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                 {clickableLine}
                 {inkLine}
             </svg>
-        );
+            {!closed ? (null) :
+                <div className="inkStroke-text" style={{
+                    color: StrCast(this.layoutDoc.textColor, "black"),
+                    pointerEvents: this.props.isDocumentActive?.() ? "all" : undefined,
+                    width: this.layoutDoc[WidthSym](),
+                }}>
+                    <FormattedTextBox
+                        {...OmitKeys(this.props, ['children']).omit}
+                        yPadding={10}
+                        xPadding={10}
+                        fieldKey={"text"}
+                        fontSize={12}
+                        dontRegisterView={true}
+                        noSidebar={true}
+                        dontScale={true}
+                        isContentActive={this.isContentActive}
+                    />
+                </div>
+            }
+        </div>;
     }
 }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index aa53f751d..d9c417b41 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1140,10 +1140,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
             });
-            const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
-            if (startupText) {
-                const { state: { tr }, dispatch } = this._editorView;
-                dispatch(tr.insertText(startupText));
+            const { state, dispatch } = this._editorView;
+            if (!rtfField) {
+                const startupText = Field.toString(this.dataDoc[fieldKey] as Field);
+                if (startupText) {
+                    dispatch(state.tr.insertText(startupText));
+                } else {
+                    selectAll(this._editorView!.state, (tr) => {
+                        this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
+                    });
+                }
             }
             (this._editorView as any).TextView = this;
         }
-- 
cgit v1.2.3-70-g09d2


From 1c0ca018dce4392a91b37ab20b936aa137c1e8a9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 17 Nov 2021 21:09:02 -0500
Subject: changed link line curves a bit.  fixed formatted text box not to
 center text for typed notes.

---
 src/client/views/InkingStroke.tsx                  |  2 +-
 .../CollectionFreeFormLinkView.tsx                 | 34 ++++++++++++++++------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 3 files changed, 27 insertions(+), 11 deletions(-)

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

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 95dad5c6e..59efb36dd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -247,7 +247,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
         // Invisible polygonal line that enables the ink to be selected by the user.
         const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
-            inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+            inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
             StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
             StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
             undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 9769453a0..9cc887e3d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -153,28 +153,44 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         const atop = this.visibleY(adiv);
         const btop = this.visibleY(bdiv);
         if (!a.width || !b.width) return undefined;
+        const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+        const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+        const acentX = (a.left + a.right) / 2;
+        const acentY = (a.top + a.bottom) / 2;
+        const bcentX = (b.left + b.right) / 2;
+        const bcentY = (b.top + b.bottom) / 2;
+        const pt1Arc = ((acentX - aDocBounds.left) > 0.1 && (aDocBounds.right - acentX) > 0.1) ||
+            ((acentY - aDocBounds.top) > 0.1 && (aDocBounds.bottom - acentY) > 0.1);
+        const pt2Arc = ((bcentX - bDocBounds.left) > 0.1 && (bDocBounds.right - bcentX) > 0.1) ||
+            ((bcentY - bDocBounds.top) > 0.1 && (bDocBounds.bottom - bcentY) > 0.1);
         const atop2 = this.visibleY(adiv);
         const btop2 = this.visibleY(bdiv);
         const aleft = this.visibleX(adiv);
         const bleft = this.visibleX(bdiv);
         const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top;
-        const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
-        const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, aleft, atop, a.width, a.height, apt.point.x, apt.point.y);
-        const pt1 = [apt.point.x, apt.point.y];
-        const pt2 = [bpt.point.x, bpt.point.y];
-        const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)];
-        const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)];
+        const pt1 = [aleft + a.width / 2, atop + a.height / 2];
+        const pt2 = [bleft + b.width / 2, btop + b.width / 2];
+        const pt1vec = [pt1[0] - (aDocBounds.left + aDocBounds.right) / 2, pt1[1] - (aDocBounds.top + aDocBounds.bottom) / 2];
+        const pt2vec = [pt2[0] - (bDocBounds.left + bDocBounds.right) / 2, pt2[1] - (bDocBounds.top + bDocBounds.bottom) / 2];
         const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1]));
         const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1]));
         const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
-        const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen];
-        const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen];
+        const pt1norm = clipped ? [0, 0] : !pt1Arc ? [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen] :
+            Math.abs(acentY - aDocBounds.top) < 0.01 ||
+                Math.abs(acentY - aDocBounds.bottom) < 0.01 ? [0, (pt2[1] - pt1[1]) / 2] : [(pt2[0] - pt1[0]) / 2, 0];
+        const pt2norm = clipped ? [0, 0] : !pt2Arc ? [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen] :
+            Math.abs(bcentY - bDocBounds.top) < 0.01 ||
+                Math.abs(bcentY - bDocBounds.bottom) < 0.01 ? [0, (pt1[1] - pt2[1]) / 2] : [(pt1[0] - pt2[0]) / 2, 0];
+        const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1;
+        const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1;
+        const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen];
+        const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen];
         const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc);
         const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc);
 
         const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
         const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
-        return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 };
+        return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
     }
 
     render() {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d9c417b41..77f33b432 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1145,7 +1145,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 const startupText = Field.toString(this.dataDoc[fieldKey] as Field);
                 if (startupText) {
                     dispatch(state.tr.insertText(startupText));
-                } else {
+                } else if (!FormattedTextBox.LiveTextUndo) {
                     selectAll(this._editorView!.state, (tr) => {
                         this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
                     });
-- 
cgit v1.2.3-70-g09d2


From 91247d583e5e4c7205a1ed764dd0e3a12af3be25 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Nov 2021 23:37:49 -0500
Subject: fixed warnings/errors.  added inkingStroke comments.  need to
 double-click now to add a point to an ink stroke.

---
 src/client/util/bezierFit.ts                       | 242 ++++++++++-----------
 src/client/views/DocumentDecorations.tsx           |   8 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/InkControlPtHandles.tsx           |  34 ++-
 src/client/views/InkStroke.scss                    |   5 +-
 src/client/views/InkStrokeProperties.ts            |  15 +-
 src/client/views/InkTangentHandles.tsx             |  28 +--
 src/client/views/InkingStroke.tsx                  | 174 ++++++++-------
 src/client/views/MainView.tsx                      |   1 -
 src/client/views/PropertiesView.tsx                |  21 +-
 src/client/views/StyleProvider.tsx                 |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +-
 src/client/views/search/SearchBox.tsx              |  18 +-
 13 files changed, 269 insertions(+), 283 deletions(-)

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

diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 57c6dbbde..784bb2e18 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -1,5 +1,4 @@
 import { Point } from "../../pen-gestures/ndollar";
-import { max } from "lodash";
 
 class SmartRect {
     minx: number = 0;
@@ -21,45 +20,38 @@ class SmartRect {
 
     public ContainsPercentage(other: SmartRect, axis: Point) {
         var ret = 0;
-        var minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y);
-        var maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y);
-        ret = maxx > minx ? (maxx - minx) / (axis == new Point(1, 0) ? other.Width : other.Height) : 0;
+        const minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y);
+        const maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y);
+        ret = maxx > minx ? (maxx - minx) / (axis === new Point(1, 0) ? other.Width : other.Height) : 0;
         return ret;
     }
     public static Bounds(p: Point[]) {
-        var r = new SmartRect();
+        const r = new SmartRect();
         if (p.length > 0) {
             r.minx = p[0].X; // These are the most likely to be extremal
             r.maxx = p.lastElement().X;
             r.miny = p[0].Y;
             r.maxy = p.lastElement().Y;
 
-            if (r.minx > r.maxx) {
-                var tmp = r.minx;
-                r.minx = r.maxx;
-                r.maxx = tmp;
-            }
-            if (r.miny > r.maxy) {
-                var tmp = r.miny;
-                r.miny = r.maxy;
-                r.maxy = tmp;
-            }
+            if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx];
+            if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny];
 
-            for (var pt of p) {
-                if (pt.X < r.minx)
+            for (const pt of p) {
+                if (pt.X < r.minx) {
                     r.minx = pt.X;
-                else if (pt.X > r.maxx)
+                } else if (pt.X > r.maxx) {
                     r.maxx = pt.X;
-
-                if (pt.Y < r.miny)
+                }
+                if (pt.Y < r.miny) {
                     r.miny = pt.Y;
-                else if (pt.Y > r.maxy)
+                } else if (pt.Y > r.maxy) {
                     r.maxy = pt.Y;
+                }
             }
         }
         return r;
     }
-};
+}
 
 function Normalize(p: Point) {
     const len = Math.sqrt(p.X * p.X + p.Y * p.Y);
@@ -67,7 +59,7 @@ function Normalize(p: Point) {
 }
 
 function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) {
-    var uPrime = new Array<number>(last - first + 1); //  New parameter values
+    const uPrime = new Array<number>(last - first + 1); //  New parameter values
 
     for (var i = first; i <= last; i++) {
         uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]);
@@ -76,36 +68,36 @@ function ReparameterizeBezier(d: Point[], first: number, last: number, u: number
 }
 function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) {
     var maxError = 0; //  Maximum error
-
     var splitPoint2D = (last - first + 1) / 2;
     for (var i = first + 1; i < last; i++) {
-        var P = [0, 0];  //  point on curve
+        const P = [0, 0];  //  point on curve
         EvalBezierFast(bezCurve, u[i - first], P);
-        var dx = P[0] - d[i].X;//  offset from point to curve
-        var dy = P[1] - d[i].Y;
-        var dist = Math.sqrt(dx * dx + dy * dy); //  Current error
+        const dx = P[0] - d[i].X;//  offset from point to curve
+        const dy = P[1] - d[i].Y;
+        const dist = Math.sqrt(dx * dx + dy * dy); //  Current error
         if (dist >= maxError) {
             maxError = dist;
-            if (splitPoint2D)
+            if (splitPoint2D) {
                 splitPoint2D = i;
+            }
         }
     }
     return { maxError, splitPoint2D };
 }
 function ChordLengthParameterize(d: Point[], first: number, last: number) {
-    var u = new Array<number>(last - first + 1);//  Parameterization
+    const u = new Array<number>(last - first + 1);//  Parameterization
 
     var prev = 0.0;
     u[0] = prev;
     for (var i = first + 1; i <= last; i++) {
-        var lastd = d[i - 1];
-        var curd = d[i];
-        var dx = lastd.X - curd.X;
-        var dy = lastd.Y - curd.Y;
+        const lastd = d[i - 1];
+        const curd = d[i];
+        const dx = lastd.X - curd.X;
+        const dy = lastd.Y - curd.Y;
         prev = u[i - first] = prev + Math.sqrt(dx * dx + dy * dy);
     }
 
-    var ulastfirst = u[last - first];
+    const ulastfirst = u[last - first];
     for (var i = first + 1; i <= last; i++) {
         u[i - first] /= ulastfirst;
     }
@@ -116,12 +108,12 @@ function ChordLengthParameterize(d: Point[], first: number, last: number) {
 *  B0, B1, B2, B3 :
 *  Bezier multipliers
 */
-function B0(u: number) { var tmp = 1.0 - u; return tmp * tmp * tmp; }
-function B1(u: number) { var tmp = 1.0 - u; return 3 * u * tmp * tmp; }
-function B2(u: number) { var tmp = 1.0 - u; return 3 * u * u * tmp; }
+function B0(u: number) { const tmp = 1.0 - u; return tmp * tmp * tmp; }
+function B1(u: number) { const tmp = 1.0 - u; return 3 * u * tmp * tmp; }
+function B2(u: number) { const tmp = 1.0 - u; return 3 * u * u * tmp; }
 function B3(u: number) { return u * u * u; }
 function bounds(p: Point[]) {
-    var r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal
+    const r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal
 
     if (r.minx > r.maxx) (r.minx, r.maxx);
     if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max
@@ -137,10 +129,9 @@ function bounds(p: Point[]) {
 }
 
 
-
 function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
-    var sz = 4;
-    var Vtemp = new Array<Array<Point>>(4);
+    const sz = 4;
+    const Vtemp = new Array<Array<Point>>(4);
     for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4);
 
     /* Copy control points */
@@ -153,8 +144,8 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
     /* Triangle computation */
     for (var i = 1; i < sz; i++) {
         for (var j = 0; j < sz - i; j++) {
-            var a = Vtemp[i - 1][j];
-            var b = Vtemp[i - 1][j + 1];
+            const a = Vtemp[i - 1][j];
+            const b = Vtemp[i - 1][j + 1];
             Vtemp[i][j].X = b.X * t + a.X * (1 - t);
             Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t);  // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t);
         }
@@ -214,14 +205,14 @@ function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
 */
 function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) {
     if (deptha > 0) {
-        var a1 = new Array<Point>(4), a2 = new Array<Point>(4);
+        const a1 = new Array<Point>(4), a2 = new Array<Point>(4);
         splitCubic(a, 0.5, a1, a2);
-        var tmid = (t0 + t1) * 0.5;
+        const tmid = (t0 + t1) * 0.5;
         deptha--;
         if (depthb > 0) {
-            var b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
             splitCubic(b, 0.5, b1, b2);
-            var umid = (u0 + u1) * 0.5;
+            const umid = (u0 + u1) * 0.5;
             depthb--;
             if (SmartRect.Intersect(bounds(a1), bounds(b1))) {
                 recursively_intersect(a1, t0, tmid, deptha, b1, u0, umid, depthb, parameters);
@@ -247,9 +238,9 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe
     }
     else {
         if (depthb > 0) {
-            var b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+            const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
             splitCubic(b, 0.5, b1, b2);
-            var umid = (u0 + u1) * 0.5;
+            const umid = (u0 + u1) * 0.5;
             depthb--;
             if (SmartRect.Intersect(bounds(a), bounds(b1))) {
                 recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters);
@@ -260,20 +251,20 @@ function recursively_intersect(a: Point[], t0: number, t1: number, deptha: numbe
         }
         else // Both segments are fully subdivided; now do line segments
         {
-            var xlk = a[3].X - a[0].X;
-            var ylk = a[3].Y - a[0].Y;
-            var xnm = b[3].X - b[0].X;
-            var ynm = b[3].Y - b[0].Y;
-            var xmk = b[0].X - a[0].X;
-            var ymk = b[0].Y - a[0].Y;
-            var det = xnm * ylk - ynm * xlk;
-            if (1.0 + det == 1.0) {
+            const xlk = a[3].X - a[0].X;
+            const ylk = a[3].Y - a[0].Y;
+            const xnm = b[3].X - b[0].X;
+            const ynm = b[3].Y - b[0].Y;
+            const xmk = b[0].X - a[0].X;
+            const ymk = b[0].Y - a[0].Y;
+            const det = xnm * ylk - ynm * xlk;
+            if (1.0 + det === 1.0) {
                 return;
             }
             else {
-                var detinv = 1.0 / det;
-                var s = (xnm * ymk - ynm * xmk) * detinv;
-                var t = (xlk * ymk - ylk * xmk) * detinv;
+                const detinv = 1.0 / det;
+                const s = (xnm * ymk - ynm * xmk) * detinv;
+                const t = (xlk * ymk - ylk * xmk) * detinv;
                 if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) {
                     return;
                 }
@@ -296,7 +287,7 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
         return;
     }
 
-    var Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points
+    const Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points
 
     /* Copy array   */
     for (var i = 0; i <= degree; i++) {
@@ -317,14 +308,11 @@ function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
 }
 
 function EvalBezierFast(p: Point[], t: number, result: number[]) {
-    var n = 3;
-    var u: number, bc: number, tn: number, tmpX: number, tmpY: number;
-    u = 1.0 - t;
-    bc = 1;
-    tn = 1;
-
-    tmpX = p[0].X * u;
-    tmpY = p[0].Y * u;
+    const n = 3;
+    const u = 1.0 - t;
+    var bc = 1, tn = 1;
+    var tmpX = p[0].X * u;
+    var tmpY = p[0].Y * u;
     tn = tn * t;
     bc = bc * (n - 1 + 1) / 1;
     tmpX = (tmpX + tn * bc * p[1].X) * u;
@@ -342,22 +330,21 @@ function EvalBezierFast(p: Point[], t: number, result: number[]) {
 *Approximate unit tangents at endpoints and "center" of digitized curve
 */
 function ComputeLeftTangent(d: Point[], end: number) {
-    var use = 1;
-    var tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y);
+    const use = 1;
+    const tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y);
     return Normalize(tHat1);
 }
 function ComputeRightTangent(d: Point[], end: number) {
-    var available = end;
-    var use = 1;
-    var tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y);
+    const use = 1;
+    const tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y);
     return Normalize(tHat2);
 }
 function ComputeCenterTangent(d: Point[], center: number) {
-    if (center == 0) {
+    if (center === 0) {
         return ComputeLeftTangent(d, center);
     }
-    var V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1];
-    var V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1];
+    const V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1];
+    const V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1];
     var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0);
     if (tHatCenter === new Point(0, 0)) {
         tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp();
@@ -365,15 +352,15 @@ function ComputeCenterTangent(d: Point[], center: number) {
     return Normalize(tHatCenter);
 }
 function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) {
-    var nPts = last - first + 1; // Number of pts in sub-curve
-    var Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
-    var Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const nPts = last - first + 1; // Number of pts in sub-curve
+    const Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
+    const Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn  //std::vector<Vector2D> A(nPts * 2);
 
     /* Compute the A's  */
     for (var i = 0; i < nPts; i++) {
-        var uprime = uPrime[i];
-        var b1 = B1(uprime);
-        var b2 = B2(uprime);
+        const uprime = uPrime[i];
+        const b1 = B1(uprime);
+        const b2 = B2(uprime);
         Ax[i] = tHat1.X * b1;
         Ay[i] = tHat1.Y * b1;
         Ax[i + 1 * nPts] = tHat2.X * b2;
@@ -381,44 +368,41 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
     }
 
     /* Create the C and X matrices  */
-    var C = [[0, 0], [0, 0]];
-    var df = d[first];
-    var dl = d[last];
+    const C = [[0, 0], [0, 0]];
+    const df = d[first];
+    const dl = d[last];
 
-    var X = [0, 0];    // Matrix X
+    const X = [0, 0];    // Matrix X
     for (var i = 0; i < nPts; i++) {
         C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i];     //A[i+0*nPts].Dot(A[i+0*nPts]);
         C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]);
         C[1][0] = C[0][1];
         C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]);
-        var uprime = uPrime[i];
-        var b0plb1 = B0(uprime) + B1(uprime);
-        var b2plb3 = B2(uprime) + B3(uprime);
-        var df1 = d[first + i];
-        var tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3));
-        var tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3));
+        const uprime = uPrime[i];
+        const b0plb1 = B0(uprime) + B1(uprime);
+        const b2plb3 = B2(uprime) + B3(uprime);
+        const df1 = d[first + i];
+        const tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3));
+        const tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3));
 
         X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp)
         X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp)
     }
 
     /* Compute the determinants of C and X  */
-    var det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
-    var det_C0_X = C[0][0] * X[1] - C[0][1] * X[0];
-    var det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
+    const det_C0_C1 = (C[0][0] * C[1][1] - C[1][0] * C[0][1]) || (C[0][0] * C[1][1]) * 10e-12;
+    const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0];
+    const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
 
     /* Finally, derive alpha values */
-    if (det_C0_C1 == 0.0) {
-        det_C0_C1 = (C[0][0] * C[1][1]) * 10e-12;
-    }
-    var alpha_l = (det_C0_C1 == 0) ? 0.0 : det_X_C1 / det_C0_C1;
-    var alpha_r = (det_C0_C1 == 0) ? 0.0 : det_C0_X / det_C0_C1;
+    var alpha_l = (det_C0_C1 === 0) ? 0.0 : det_X_C1 / det_C0_C1;
+    var alpha_r = (det_C0_C1 === 0) ? 0.0 : det_C0_X / det_C0_C1;
 
     /* If alpha negative, use the Wu/Barsky heuristic (see text) */
     /* (if alpha is 0, you get coincident control points that lead to
     * divide by zero in any subsequent NewtonRaphsonRootFind() call. */
-    var segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y));
-    var epsilon = 1.0e-6 * segLength;
+    const segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y));
+    const epsilon = 1.0e-6 * segLength;
     if (alpha_l < epsilon || alpha_r < epsilon) {
         /* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */
         alpha_l = alpha_r = segLength / 3.0;
@@ -432,15 +416,15 @@ function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[
     result[3] = dl;
     result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l));
     result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r));
-
 }
+
 /*
  *  NewtonRaphsonRootFind :
  *  Use Newton-Raphson iteration to find better root.
  */
 function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
-    var Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; //  Q' and Q''
-    var Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q''
+    const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; //  Q' and Q''
+    const Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q''
 
     /* Compute Q(u) */
     var uPrime: number; //  Improved u
@@ -463,25 +447,24 @@ function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
     EvalBezier(Q2, 1, u, Q2_u);
 
     /* Compute f(u)/f'(u) */
-    var numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]);
-    var denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]);
-    if (denominator == 0.0)
+    const numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]);
+    const denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]);
+    if (denominator === 0.0) {
         uPrime = u;
-    else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */
+    } else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */
 
     return uPrime;
 }
 function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) {
-    var bezCurve = new Array<Point>(4);          //  Control points of fitted Bezier curve
-    var splitPoint2D: number;           //  Point2D to split point set at
-    var maxIterations = 4;      //  Max times to try iterating
+    const bezCurve = new Array<Point>(4);          //  Control points of fitted Bezier curve
+    const maxIterations = 4;      //  Max times to try iterating
 
-    var iterationError = error * error; //  Error below which you try iterating
-    var nPts = last - first + 1;        //  Number of points in subset
+    const iterationError = error * error; //  Error below which you try iterating
+    const nPts = last - first + 1;        //  Number of points in subset
 
     /*  Use heuristic if region only has two points in it */
-    if (nPts == 2) {
-        var dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3;
+    if (nPts === 2) {
+        const dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3;
 
         bezCurve[0] = d[first];
         bezCurve[3] = d[last];
@@ -499,7 +482,7 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve);
 
     /*  Find max deviation of points to fitted curve */
-    var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u);  //  Maximum fitting error
+    const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u);  //  Maximum fitting error
     if (maxError < Math.abs(error)) {
         result.push(bezCurve[1]);
         result.push(bezCurve[2]);
@@ -511,9 +494,9 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     /*  and iteration */
     if (maxError < iterationError) {
         for (var i = 0; i < maxIterations; i++) {
-            var uPrime = ReparameterizeBezier(d, first, last, u, bezCurve);   //  Improved parameter values
+            const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve);   //  Improved parameter values
             GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve);
-            var { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, uPrime);
+            const { maxError } = ComputeMaxError(d, first, last, bezCurve, uPrime);
             if (maxError < error) {
                 result.push(bezCurve[1]);
                 result.push(bezCurve[2]);
@@ -525,16 +508,15 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
     }
 
     /* Fitting failed -- split at max error point and fit recursively */
-    var tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D);
+    const tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D);
     FitCubic(d, first, splitPoint2D, tHat1, tHatCenter, error, result);
-    var negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
+    const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
     FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result);
 }
 export function FitCurve(d: Point[], error: number) {
-    var tHat1 = ComputeLeftTangent(d, 0); //  Unit tangent vectors at endpoints
-    var tHat2 = ComputeRightTangent(d, d.length - 1);
-    var result: Point[] = [];
-    result.push(d[0]);
+    const tHat1 = ComputeLeftTangent(d, 0); //  Unit tangent vectors at endpoints
+    const tHat2 = ComputeRightTangent(d, d.length - 1);
+    const result = [d[0]];
     FitCubic(d, 0, d.length - 1, tHat1, tHat2, error, result);
     return result;
 }
@@ -543,14 +525,14 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) {
     tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1));
     tHat2 = new Point(-tHat2.X, -tHat2.Y);
     var u = ChordLengthParameterize(d, 0, d.length - 1);
-    var bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)];
+    const bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)];
     GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls);    /*  Find max deviation of points to fitted curve */
     var finalCtrls = bezCurveCtrls.slice();
     var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u);
     for (var i = 0; i < 10; i++) {
-        var uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls);   //  Improved parameter values
+        const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls);   //  Improved parameter values
         GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls);
-        var { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime);
+        const { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime);
         if (maxError < error) {
             error = maxError;
             finalCtrls = bezCurveCtrls.slice();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 1c0b1b995..7d3959eba 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -203,9 +203,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
                 const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 };
                 const previousPoint = { X: e.clientX, Y: e.clientY };
                 const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
-                const angle = InkStrokeProperties.Instance?.angleChange(previousPoint, movedPoint, centerPoint);
+                const angle = InkStrokeProperties.Instance.angleChange(previousPoint, movedPoint, centerPoint);
                 const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
-                angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt);
+                angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, pt);
                 return false;
             },
             () => {
@@ -226,7 +226,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
         this._inkDragDocs = DragManager.docsBeingDragged
             .filter(doc => doc.type === DocumentType.INK)
             .map(doc => {
-                if (InkStrokeProperties.Instance?._lock) {
+                if (InkStrokeProperties.Instance._lock) {
                     Doc.SetNativeHeight(doc, NumCast(doc._height));
                     Doc.SetNativeWidth(doc, NumCast(doc._width));
                 }
@@ -249,7 +249,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
         const first = SelectionManager.Views()[0];
         let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
         var fixedAspect = Doc.NativeAspect(first.layoutDoc);
-        InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
+        InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
             .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc));
 
         const resizeHdl = this._resizeHdlId.split(" ")[0];
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 364bf05e2..d5e0ed962 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -112,7 +112,7 @@ export class KeyManager {
             case "escape":
                 DocumentLinksButton.StartLink = undefined;
                 DocumentLinksButton.StartLinkView = undefined;
-                InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false);
+                InkStrokeProperties.Instance._controlButton = false;
                 CurrentUserUtils.SelectedTool = InkTool.None;
                 var doDeselect = true;
                 if (SnappingManager.GetIsDragging()) {
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index a91e74c44..76ce73b0d 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -44,23 +44,23 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
         const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
-        if (InkStrokeProperties.Instance && ptFromScreen) {
+        if (ptFromScreen) {
             const order = controlIndex % 4;
             const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
             const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
             const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
-            const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex;
+            const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
             setupMoveUpEvents(this, e,
                 action((e: PointerEvent, down: number[], delta: number[]) => {
                     if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
                     const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
                     const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+                    InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
                     return false;
                 }),
                 action(() => {
                     if (this.controlUndo) {
-                        InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex);
+                        InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex);
                     }
                     this.controlUndo?.end();
                     this.controlUndo = undefined;
@@ -75,11 +75,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
                         } else {
                             if (brokenIndices?.includes(equivIndex)) {
                                 if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
-                                InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
+                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
                             }
                             if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
                                 if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
-                                InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
+                                InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
                             }
                         }
                         this.controlUndo?.end();
@@ -102,7 +102,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
     @action
     onDelete = (e: KeyboardEvent) => {
         if (["-", "Backspace", "Delete"].includes(e.key)) {
-            InkStrokeProperties.Instance?.deletePoints(this.props.inkView, e.shiftKey);
+            InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey);
             e.stopPropagation();
         }
     }
@@ -111,11 +111,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
      * Changes the current selected control point.
      */
     @action
-    changeCurrPoint = (i: number) => {
-        if (InkStrokeProperties.Instance) {
-            InkStrokeProperties.Instance._currentPoint = i;
-        }
-    }
+    changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i
 
     render() {
         // Accessing the current ink's data and extracting all control points.
@@ -202,9 +198,9 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             const scaling = v2len / v1len;
             const v1n = { x: v1.x / v1len, y: v1.y / v1len };
             const v2n = { x: v2.x / v2len, y: v2.y / v2len };
-            const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y)
-            InkStrokeProperties.Instance?.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
-            InkStrokeProperties.Instance?.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
+            const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y);
+            InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
+            InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
             return false;
         }, action(() => {
             this.controlUndo?.end();
@@ -214,7 +210,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
     }
 
     render() {
-        const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={"npt"}
+        const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key}
             cx={pt.X}
             cy={pt.Y}
             r={this.props.screenSpaceLineWidth * 2}
@@ -223,12 +219,12 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
             strokeWidth={0}
             onPointerLeave={action(() => this._overStart = false)}
             onPointerEnter={action(() => this._overStart = true)}
-            onPointerDown={e => dragFunc(e)}
+            onPointerDown={dragFunc}
             pointerEvents="all"
         />;
         return (<svg>
-            {hdl(this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
-            {hdl(this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
+            {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
+            {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
         </svg>
         );
     }
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 2127826b4..664f2448b 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -14,6 +14,9 @@
 }
 
 .inkStroke-wrapper {
+    display: flex;
+    align-items: center;
+    height: 100%;
     .inkStroke {
         mix-blend-mode: multiply;
         stroke-linejoin: round;
@@ -22,7 +25,7 @@
         transform-origin: top left;
         width: 100%;
         height: 100%;
-
+        pointer-events: none;
         svg:not(:root) {
             overflow: visible !important;
         }
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 4808bbc77..7ab631b03 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -15,14 +15,15 @@ import { InkingStroke } from "./InkingStroke";
 import { DocumentView } from "./nodes/DocumentView";
 
 export class InkStrokeProperties {
-    static Instance: InkStrokeProperties | undefined;
+    static _Instance: InkStrokeProperties | undefined;
+    public static get Instance() { return this._Instance || new InkStrokeProperties(); }
 
     @observable _lock = false;
     @observable _controlButton = false;
     @observable _currentPoint = -1;
 
     constructor() {
-        InkStrokeProperties.Instance = this;
+        InkStrokeProperties._Instance = this;
         reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None));
         reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false));
     }
@@ -150,15 +151,15 @@ export class InkStrokeProperties {
         } else {
             const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
             const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-            var samples: Point[] = [];
+            const samples: Point[] = [];
             var startDir = { x: 0, y: 0 };
             var endDir = { x: 0, y: 0 };
             for (var i = 0; i < splicedPoints.length / 4; i++) {
-                var bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
                 if (i === 0) startDir = bez.derivative(0);
                 if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
                 for (var t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
-                    var pt = bez.compute(t);
+                    const pt = bez.compute(t);
                     samples.push(new Point(pt.x, pt.y));
                 }
             }
@@ -209,12 +210,12 @@ export class InkStrokeProperties {
             const ptFromScreen = view.ComponentView?.ptFromScreen;
             const ptToScreen = view.ComponentView?.ptToScreen;
             return !ptToScreen || !ptFromScreen ? ink :
-                ink.map(i => ptToScreen(i)).map(i => {
+                ink.map(ptToScreen).map(i => {
                     const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y };
                     const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling;
                     const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x);
                     const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x };
-                    const newpt = ptFromScreen!(newscrpt);
+                    const newpt = ptFromScreen(newscrpt);
                     return newpt;
                 });
         });
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index f88a20448..ab73e58a4 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -29,24 +29,23 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
      * @param handleNum The index of the currently selected handle point.
      */
     onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
-        if (InkStrokeProperties.Instance) {
-            var controlUndo: UndoManager.Batch | undefined;
-            const screenScale = this.props.ScreenToLocalTransform().Scale;
-            const order = handleIndex % 4;
-            const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
-            const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
-            const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
-            setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
+        var controlUndo: UndoManager.Batch | undefined;
+        const screenScale = this.props.ScreenToLocalTransform().Scale;
+        const order = handleIndex % 4;
+        const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
+        const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
+        const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
+        setupMoveUpEvents(this, e,
+            (e: PointerEvent, down: number[], delta: number[]) => {
                 if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
                 if (e.altKey) this.onBreakTangent(controlIndex);
-                InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
+                InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
                 return false;
             }, () => {
                 controlUndo?.end();
                 UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
             }, emptyFunction
-            );
-        }
+        );
     }
 
     /**
@@ -66,9 +65,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
     }
 
     render() {
-        const formatInstance = InkStrokeProperties.Instance;
-        if (!formatInstance) return (null);
-
         // Accessing the current ink's data and extracting all handle points and handle lines.
         const data = this.props.screenCtrlPoints;
         const tangentHandles: HandlePoint[] = [];
@@ -107,7 +103,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             onPointerDown={e => this.onHandleDown(e, pts.I)}
                             pointerEvents="all"
                             cursor="default"
-                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
+                            display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />
                     </svg>)}
                 {tangentLines.map((pts, i) => {
                     const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
@@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
                             stroke={Colors.MEDIUM_BLUE}
                             strokeDasharray={"1 1"}
                             strokeWidth={1}
-                            display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />;
+                            display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />;
                     return <svg height="100" width="100" key={`line${i}`}>
                         {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
                         {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8ff080f81..0b3619b22 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,3 +1,25 @@
+/*
+    InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled.
+
+    The primary data is:
+        data - an InkField which is an array of PointData (X,Y values).  The data is laid out as a sequence of simple bezier segments:
+              point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ...  (Note that segment endpoints are duplicated ie Point2 = Point 3)
+        brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
+        text - a text field that will be centered within a closed ink stroke
+        isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by 
+               the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.
+
+    The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
+        Thus the mapping can roughly be described by:
+        the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView
+        the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView
+        NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space
+
+    InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in 
+        screen space (to avoid scaling artifacts)
+    
+    Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
+*/
 import React = require("react");
 import { action, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
@@ -30,35 +52,32 @@ const InkDocument = makeInterface(documentSchema);
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+    static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
     public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
-    static readonly MaskDim = 50000;
     public static IsClosed(inkData: InkData) {
         return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
     }
-    @observable private _properties?: InkStrokeProperties;
-    _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
-    _selDisposer: IReactionDisposer | undefined;
+    private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
+    private _selDisposer?: IReactionDisposer;
 
-    @observable _nearestT: number | undefined;
-    @observable _nearestSeg: number | undefined;
-    @observable _nearestScrPt: { X: number, Y: number } | undefined;
-    @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
-
-    constructor(props: FieldViewProps & InkDocument) {
-        super(props);
-
-        this._properties = InkStrokeProperties.Instance;
-    }
+    @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
+    @observable _nearestT?: number;  // nearest t value within the nearest Bezier segment "
+    @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke ""
 
     componentDidMount() {
         this.props.setContentView?.(this);
         this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
-            selected => !selected && this.toggleControlButton());
+            selected => !selected && (InkStrokeProperties.Instance._controlButton = false));
     }
     componentWillUnmount() {
         this._selDisposer?.();
     }
 
+    /**
+    * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); 
+    * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
+    * the center of the ink stroke changes as the stroke is rotated.
+    */
     getCenter = (xf: Transform) => {
         const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
         const angle = -NumCast(this.layoutDoc.rotation);
@@ -80,6 +99,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
     }
 
+    /**
+     * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
+     *    When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
+     *  appear to illuminate what it covers up.  At the same time, all pixels that are not under the stroke will be
+     *  dimmed by a semi-opaque overlay mask.
+    */
     public static toggleMask = action((inkDoc: Doc) => {
         inkDoc.isInkMask = !inkDoc.isInkMask;
         inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
@@ -88,70 +113,47 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
     });
     /**
-     * Handles the movement of the entire ink object when the user clicks and drags.
+     * Drags the a simple bezier segment of the stroke.
+     * Also adds a control point when double clicking on the stroke.
      */
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        const ptFromScreen = this.ptFromScreen;
         this._handledClick = false;
-        if (InkStrokeProperties.Instance && ptFromScreen) {
-            const inkView = this.props.docViewPath().lastElement();
-            const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
-            const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
-                (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
-                (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
-            const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
-            const controlIndex = nearestSeg;
-            const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex;
-            var controlUndo: UndoManager.Batch | undefined;
-            const isEditing = this._properties?._controlButton && this.props.isSelected();
-            setupMoveUpEvents(this, e,
-                !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => {
-                    if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
-                    const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
-                    const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
-                    InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
-                    InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
-                    return false;
-                }),
-                !isEditing ? returnFalse : action(() => {
-                    if (controlUndo) {
-                        InkStrokeProperties.Instance?.snapControl(inkView, controlIndex);
-                        InkStrokeProperties.Instance?.snapControl(inkView, controlIndex + 3);
-                    }
-                    controlUndo?.end();
-                    controlUndo = undefined;
-                    UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
-                }),
-                action((e: PointerEvent, doubleTap: boolean | undefined) => {
-                    doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
-                    if (doubleTap && this._properties) {
-                        this._properties._controlButton = true;
-                        InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
-                        this._handledClick = true;  // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
-                    } else if (isEditing) {
-                        this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
+        const inkView = this.props.docViewPath().lastElement();
+        const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+        const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
+            (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+            (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
+        const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
+        const controlIndex = nearestSeg;
+        const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
+        var controlUndo: UndoManager.Batch | undefined;
+        const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
+        setupMoveUpEvents(this, e,
+            !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => {
+                if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+                const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
+                const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
+                InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+                InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
+                return false;
+            }),
+            !isEditing ? returnFalse : action(() => {
+                controlUndo?.end();
+                controlUndo = undefined;
+                UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+            }),
+            action((e: PointerEvent, doubleTap: boolean | undefined) => {
+                doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
+                if (doubleTap) {
+                    InkStrokeProperties.Instance._controlButton = true;
+                    InkStrokeProperties.Instance._currentPoint = -1;
+                    this._handledClick = true;  // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
+                    if (isEditing) {
+                        this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
                     }
-                }), isEditing, isEditing, action(() => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1)));
-        }
-    }
-
-    /**
-     * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected.
-     */
-    @action
-    toggleControlButton = () => {
-        if (!this.props.isSelected() && this._properties) {
-            this._properties._controlButton = false;
-        }
-    }
-
-    @action
-    checkHighlighter = () => {
-        if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) {
-            // this._previousColor = ActiveInkColor();
-            SetActiveInkColor("rgba(245, 230, 95, 0.75)");
-        }
+                }
+            }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)));
     }
 
     ptFromScreen = (scrPt: { X: number, Y: number }) => {
@@ -173,13 +175,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         return { X: scrPt[0], Y: scrPt[1] };
     }
 
+    /**
+    * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
+    * @param scrPt - the point to snap to this stroke
+    * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
+    *
+    * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
+    */
     snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
         const { inkData } = this.inkScaledData();
-        const inkPt = this.ptFromScreen(scrPt);
-        const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []);
+        const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []);
         return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale };
     }
 
+    /**
+    * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale
+    * factor for converting between ink and screen space.
+    */
     inkScaledData = () => {
         const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
         const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
@@ -229,7 +241,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
         const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
         const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
         return SnappingManager.GetIsDragging() ? (null) :
-            !this._properties?._controlButton ?
+            !InkStrokeProperties.Instance._controlButton ?
                 (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
                     <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
                         <InkEndPtHandles
@@ -287,15 +299,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
             false, downHdlr);
         // Set of points rendered upon the ink that can be added if a user clicks on one.
 
-        return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
+        return <div className="inkStroke-wrapper">
             <svg className="inkStroke"
                 style={{
-                    width: "100%",
-                    height: "100%",
-                    pointerEvents: "none",
                     transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
                     mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
-                    overflow: "visible",
                     cursor: this.props.isSelected() ? "default" : undefined
                 }}
                 onPointerLeave={action(e => this._nearestScrPt = undefined)}
@@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
                     const cm = ContextMenu.Instance;
                     !Doc.UserDoc().noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
                     cm?.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
-                    cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
+                    cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" });
                 }}
             >
                 {clickableLine(this.onPointerDown)}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 546b0e360..a9fea4a78 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -116,7 +116,6 @@ export class MainView extends React.Component {
             }, 0);
             setTimeout(() => ele.outerHTML = '', 1000);
         }
-        new InkStrokeProperties();
         this._sidebarContent.proto = undefined;
         if (!MainView.Live) {
             DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 1083e0075..18d5f1642 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -535,16 +535,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     @computed
     get controlPointsButton() {
-        const formatInstance = InkStrokeProperties.Instance;
-        return !formatInstance ? (null) : <div className="inking-button">
+        return <div className="inking-button">
             <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}>
-                <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}>
+                <div className="inking-button-points"
+                    style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? "black" : "" }}
+                    onPointerDown={action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} >
                     <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
                 </div>
             </Tooltip>
-            <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
-                <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} >
-                    <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" />
+            <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
+                <div className="inking-button-lock" onPointerDown={action(() => InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} >
+                    <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? "lock" : "unlock"} color="white" size="lg" />
                 </div>
             </Tooltip>
             <Tooltip title={<div className="dash-tooltip">{"Rotate 90˚"}</div>}>
@@ -603,7 +604,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 const oldX = NumCast(this.selectedDoc?.x);
                 const oldY = NumCast(this.selectedDoc?.y);
                 this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10));
-                InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height)));
+                InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height)));
                 const doc = this.selectedDoc;
                 if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
                     const ink = Cast(doc.data, InkField)?.inkData;
@@ -625,7 +626,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 const oX = NumCast(this.selectedDoc?.x);
                 const oY = NumCast(this.selectedDoc?.y);
                 this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10));
-                InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width)));
+                InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width)));
                 const docu = this.selectedDoc;
                 if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
                     const ink = Cast(docu.data, InkField)?.inkData;
@@ -663,12 +664,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     set shapeWid(value) {
         const oldWidth = NumCast(this.selectedDoc?._width);
         this.selectedDoc && (this.selectedDoc._width = Number(value));
-        InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
+        InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
     }
     set shapeHgt(value) {
         const oldHeight = NumCast(this.selectedDoc?._height);
         this.selectedDoc && (this.selectedDoc._height = Number(value));
-        InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
+        InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
     }
 
     @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); }
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8d8630907..f09d532ad 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -93,7 +93,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
         case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
         case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
-        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily)));
+        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily)));
         case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
             StrCast(doc._showTitle,
                 props?.showTitle?.() ||
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9d0402075..72e84c141 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1146,7 +1146,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 if (startupText) {
                     dispatch(state.tr.insertText(startupText));
                 } else if (!FormattedTextBox.LiveTextUndo) {
-                    selectAll(this._editorView!.state, (tr) => {
+                    selectAll(this._editorView.state, (tr) => {
                         this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
                     });
                 }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index fe297782c..09cfb2077 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -251,7 +251,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
         this._results.forEach((_, doc) => {
             this._pageRanks.set(doc, 1.0 / this._results.size);
 
-            if (Doc.GetProto(doc)[DirectLinksSym].size == 0) {
+            if (Doc.GetProto(doc)[DirectLinksSym].size === 0) {
                 this._linkedDocsOut.set(doc, new Set(this._results.keys()));
 
                 this._results.forEach((_, linkedDoc) => {
@@ -259,20 +259,20 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
                 });
             }
             else {
-                let linkedDocSet: Set<Doc> = new Set();
+                const linkedDocSet = new Set<Doc>();
 
                 Doc.GetProto(doc)[DirectLinksSym].forEach((link) => {
-                    let d1 = link?.anchor1 as Doc;
-                    let d2 = link?.anchor2 as Doc;
-                    if (doc == d1 && this._results.has(d2)) {
+                    const d1 = link?.anchor1 as Doc;
+                    const d2 = link?.anchor2 as Doc;
+                    if (doc === d1 && this._results.has(d2)) {
                         linkedDocSet.add(d2);
                         this._linkedDocsIn.get(d2)?.add(doc);
                     }
-                    else if (doc == d2 && this._results.has(d1)) {
+                    else if (doc === d2 && this._results.has(d1)) {
                         linkedDocSet.add(d1);
                         this._linkedDocsIn.get(d1)?.add(doc);
                     }
-                })
+                });
 
                 this._linkedDocsOut.set(doc, linkedDocSet);
             }
@@ -291,7 +291,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
         let converged = true;
         const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size;
 
-        let nextPageRanks: Map<Doc, number> = new Map<Doc, number>();
+        const nextPageRanks = new Map<Doc, number>();
 
         this._results.forEach((_, doc) => {
             let nextPageRank = pageRankFromAll;
@@ -397,7 +397,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
 
         const isLinkSearch: boolean = this.props.linkSearch;
 
-        const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0))  // sorted by page rank
+        const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0));  // sorted by page rank
 
         const resultsJSX = Array();
 
-- 
cgit v1.2.3-70-g09d2


From 4bad9d544879f5d59becb2c0e22c9ff5800f2890 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 3 Dec 2021 14:39:40 -0500
Subject: cleaned up text alignment buttons & initialization in formatted text
 box

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/views/nodes/button/FontIconBox.tsx      | 26 ++-------------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  6 ++--
 .../views/nodes/formattedText/RichTextMenu.tsx     | 39 +++++++++-------------
 4 files changed, 24 insertions(+), 48 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5c5818f8f..80c3f9168 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -746,6 +746,7 @@ export namespace Docs {
             I.strokeEndMarker = arrowEnd;
             I.strokeDash = dash;
             I.tool = tool;
+            I["text-align"] = "center";
             I.title = "ink";
             I.x = options.x;
             I.y = options.y;
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 14b1cbb5d..bd103dcf7 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -636,30 +636,10 @@ Scripting.addGlobal(function getActiveTextInfo(info: "family" | "size" | "color"
 Scripting.addGlobal(function setAlignment(align: "left" | "right" | "center", checkResult?: boolean) {
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
     if (checkResult) {
-        let active: string;
-        if (editorView) {
-            active = editorView?.state && RichTextMenu.Instance.getActiveAlignment();
-        } else {
-            active = StrCast(Doc.UserDoc().textAlign);
-        }
-        if (active === align) return Colors.MEDIUM_BLUE;
-        return "transparent";
+        return (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : "transparent";
     }
-    SelectionManager.Docs().map(doc => doc.textAlign = align);
-    switch (align) {
-        case "left":
-            editorView?.state && RichTextMenu.Instance.alignLeft(editorView, editorView.dispatch);
-            break;
-        case "center":
-            editorView?.state && RichTextMenu.Instance.alignCenter(editorView, editorView.dispatch);
-            break;
-        case "right":
-            editorView?.state && RichTextMenu.Instance.alignRight(editorView, editorView.dispatch);
-            break;
-        default:
-            break;
-    }
-    Doc.UserDoc().textAlign = align;
+    if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align);
+    else Doc.UserDoc().textAlign = align;
 });
 
 Scripting.addGlobal(function setBulletList(mapStyle: "bullet" | "decimal", checkResult?: boolean) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e61f96852..4a6af86c1 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1146,9 +1146,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 const startupText = Field.toString(this.dataDoc[fieldKey] as Field);
                 if (startupText) {
                     dispatch(state.tr.insertText(startupText));
-                } else if (!FormattedTextBox.LiveTextUndo) {
+                }
+                const textAlign = StrCast(this.dataDoc["text-align"], StrCast(Doc.UserDoc().textAlign, "left"));
+                if (textAlign !== "left") {
                     selectAll(this._editorView.state, (tr) => {
-                        this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
+                        this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
                     });
                 }
             }
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index bd05af977..4814d6e9a 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -45,7 +45,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
     @observable private _activeFontSize: string = "13px";
     @observable private _activeFontFamily: string = "";
     @observable private activeListType: string = "";
-    @observable private activeAlignment: string = "left";
+    @observable private _activeAlignment: string = "left";
 
     @observable private brushMarks: Set<Mark> = new Set();
     @observable private showBrushDropdown: boolean = false;
@@ -86,6 +86,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
     @computed get fontColor() { return this._activeFontColor; }
     @computed get fontFamily() { return this._activeFontFamily; }
     @computed get fontSize() { return this._activeFontSize; }
+    @computed get textAlign() { return this._activeAlignment; }
 
     public delayHide = () => this._delayHide = true;
 
@@ -115,7 +116,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
         const activeHighlights = active.activeHighlights;
 
         this.activeListType = this.getActiveListStyle();
-        this.activeAlignment = this.getActiveAlignment();
+        this._activeAlignment = this.getActiveAlignment();
         this._activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
         this._activeFontSize = !activeSizes.length ? "13px" : activeSizes[0];
         this._activeFontColor = !activeColors.length ? "black" : activeColors.length > 0 ? String(activeColors[0]) : "...";
@@ -388,29 +389,21 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
         dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
         return true;
     }
-    alignCenter = (view: EditorView, dispatch: any) => {
-        return this.TextView.props.isSelected(true) && this.alignParagraphs(view, "center", dispatch);
-    }
-    alignLeft = (view: EditorView, dispatch: any) => {
-        return this.TextView.props.isSelected(true) && this.alignParagraphs(view, "left", dispatch);
-    }
-    alignRight = (view: EditorView, dispatch: any) => {
-        return this.TextView.props.isSelected(true) && this.alignParagraphs(view, "right", dispatch);
-    }
 
-    alignParagraphs(view: EditorView, align: "left" | "right" | "center", dispatch: any) {
-        var tr = view.state.tr;
-        view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => {
-            if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
-                tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks);
-                return false;
-            }
+    align = (view: EditorView, dispatch: any, alignment: "left" | "right" | "center") => {
+        if (this.TextView.props.isSelected(true)) {
+            var tr = view.state.tr;
+            view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => {
+                if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) {
+                    tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align: alignment }, node.marks);
+                    return false;
+                }
+                view.focus();
+                return true;
+            });
             view.focus();
-            return true;
-        });
-        view.focus();
-        dispatch?.(tr);
-        return true;
+            dispatch?.(tr);
+        }
     }
 
     insetParagraph(state: EditorState<any>, dispatch: any) {
-- 
cgit v1.2.3-70-g09d2

