From 5a425e5cf18115921ecb4e7cf931e65f45dab8e2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 18 Aug 2022 11:18:14 -0400
Subject: fixed up presboxelement to not reference presbox.instance.  fixing
 layout of presboxelement in treeview.  fixing "removing" an inkMask without
 removing it from the collection so that it can be turned on and off.

---
 src/client/views/DocumentDecorations.tsx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3544f74b4..6d1397395 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -203,7 +203,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 if (this._deleteAfterIconify) {
                     views.forEach(iconView => {
                         Doc.setNativeView(iconView.props.Document);
-                        iconView.props.removeDocument?.(iconView.props.Document);
+                        if (iconView.props.Document.isInkMask && iconView.props.Document.activeFrame !== undefined) {
+                            iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMaks to be "turned off" without removing them from the collection which allows them to function properly in a presenation.
+                        } else {
+                            iconView.props.removeDocument?.(iconView.props.Document);
+                        }
                     });
                     SelectionManager.DeselectAll();
                 }
-- 
cgit v1.2.3-70-g09d2


From 53dc1ae6077774a7235f2fe7f56ffa03f8a9aa5a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 19 Aug 2022 09:21:28 -0400
Subject: fixed undo bug in dragManager where batches weren't being closed. 
 fixed schemaheader copy method to copy all parameters.  fixed notetaking
 columnresizer to create an UndoBatch.  fixed notetakingview's columnHeaders
 to return the actual headers list, not a copy.  fixed document decorations to
 not modify docsBeingDragged

---
 src/client/util/DragManager.ts                     |  2 +-
 src/client/views/DocumentDecorations.tsx           |  4 +-
 .../views/collections/CollectionNoteTakingView.tsx | 20 ++---
 .../CollectionNoteTakingViewDivider.tsx            | 37 +++++----
 .../views/collections/CollectionStackingView.tsx   |  2 -
 src/client/views/nodes/trails/PresElementBox.tsx   |  5 +-
 src/fields/SchemaHeaderField.ts                    | 92 +++++++++++-----------
 7 files changed, 75 insertions(+), 87 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 947882958..eef5b9ce1 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -457,7 +457,7 @@ export namespace DragManager {
             document.removeEventListener('pointerup', upHandler, true);
             SnappingManager.SetIsDragging(false);
             SnappingManager.clearSnapLines();
-            if (undo && batch.end()) UndoManager.Undo();
+            if (batch.end() && undo) UndoManager.Undo();
             docsBeingDragged.length = 0;
         });
         var startWindowDragTimer: any;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 0db9eab69..a8dacff51 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -351,8 +351,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        DragManager.docsBeingDragged.push(...SelectionManager.Views().map(dv => dv.rootDoc));
-        this._inkDragDocs = DragManager.docsBeingDragged
+        const views = SelectionManager.Views().map(dv => dv.rootDoc);
+        this._inkDragDocs = views
             .filter(doc => doc.type === DocumentType.INK)
             .map(doc => {
                 if (InkStrokeProperties.Instance._lock) {
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 447e1f0c8..0c5f69db0 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -53,7 +53,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
         return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
     }
     @computed get columnHeaders() {
-        const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null));
+        const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
         const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders.find(sh => sh.heading === 'unset'));
 
         //  @#Oberable draggedColIndex = ...
@@ -142,17 +142,11 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
     @computed get Sections() {
         TraceMobx();
         const columnHeaders = this.columnHeaders;
-        let docs = this.childDocs;
+        // filter out the currently dragged docs from the child docs, since we will insert them later
+        const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d));
         const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
         const rowCol = this.docsDraggedRowCol;
 
-        // filter out the currently dragged docs from the child docs, since we will insert them later
-        if (rowCol.length && DragManager.docsBeingDragged.length) {
-            const docIdsToRemove = new Set();
-            DragManager.docsBeingDragged.forEach(d => docIdsToRemove.add(d[Id]));
-            docs = docs.filter(d => !docIdsToRemove.has(d[Id]));
-        }
-
         // this will sort the docs into the correct columns (minus the ones you're currently dragging)
         docs.map(d => {
             const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
@@ -165,11 +159,10 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
         });
 
         // now we add back in the docs that we're dragging
-        if (rowCol.length && DragManager.docsBeingDragged.length) {
-            const colHeader = columnHeaders[rowCol[1]];
+        if (rowCol.length) {
             // TODO: get the actual offset that occurs if the docs were in that column
             const offset = 0;
-            sections.get(colHeader)?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
+            sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
         }
         return sections;
     }
@@ -671,7 +664,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
 
     // used to reset column sizes when using the drag handlers
     @action
-    setColumnStartXCoords = (movementX: number, colIndex: number) => {
+    setColumnStartXCoords = (movementXScreen: number, colIndex: number) => {
+        const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0];
         const leftHeader = this.columnHeaders[colIndex];
         const rightHeader = this.columnHeaders[colIndex + 1];
         leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth);
diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
index 7d31b3193..2633bffeb 100644
--- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
@@ -1,5 +1,8 @@
 import { action, observable } from 'mobx';
 import * as React from 'react';
+import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { Transform } from '../../util/Transform';
+import { UndoManager } from '../../util/UndoManager';
 
 interface DividerProps {
     index: number;
@@ -13,28 +16,24 @@ export class CollectionNoteTakingViewDivider extends React.Component<DividerProp
 
     @action
     private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
-        e.stopPropagation();
-        e.preventDefault();
-        window.removeEventListener('pointermove', this.onPointerMove);
-        window.removeEventListener('pointerup', this.onPointerUp);
-        window.addEventListener('pointermove', this.onPointerMove);
-        window.addEventListener('pointerup', this.onPointerUp);
+        const batch = UndoManager.StartBatch('resizing');
+        setupMoveUpEvents(
+            this,
+            e,
+            (e, down, delta) => {
+                this.props.setColumnStartXCoords(delta[0], this.props.index);
+                return false;
+            },
+            action(() => {
+                this.isResizingActive = false;
+                this.isHoverActive = false;
+                batch.end();
+            }),
+            emptyFunction
+        );
         this.isResizingActive = true;
     };
 
-    @action
-    private onPointerUp = () => {
-        this.isResizingActive = false;
-        this.isHoverActive = false;
-        window.removeEventListener('pointermove', this.onPointerMove);
-        window.removeEventListener('pointerup', this.onPointerUp);
-    };
-
-    @action
-    onPointerMove = ({ movementX }: PointerEvent) => {
-        this.props.setColumnStartXCoords(movementX, this.props.index);
-    };
-
     render() {
         return (
             <div
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ef68cadd7..6683eddf3 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -454,8 +454,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                     newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1));
                     docs.splice(insertInd - offset, 0, ...newDocs);
                 }
-                // reset drag manager docs, because we just dropped
-                DragManager.docsBeingDragged.length = 0;
             }
         } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 0cf15d297..c578f8bcf 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -250,10 +250,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     onPointerMove = (e: PointerEvent) => {
         const slide = this._itemRef.current!;
-        let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
-        for (const doc of DragManager.docsBeingDragged) {
-            if (!doc.presentationTargetDoc) dragIsPresItem = false;
-        }
+        const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc);
         if (slide && dragIsPresItem) {
             const rect = slide.getBoundingClientRect();
             const y = e.clientY - rect.top; //y position within the element.
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 3b02d0cfe..1321bc327 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -1,60 +1,60 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, primitive } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { ColumnType } from "../client/views/collections/collectionSchema/CollectionSchemaView";
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, primitive } from 'serializr';
+import { ObjectField } from './ObjectField';
+import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
 
 export const PastelSchemaPalette = new Map<string, string>([
     // ["pink1", "#FFB4E8"],
-    ["pink2", "#ff9cee"],
-    ["pink3", "#ffccf9"],
-    ["pink4", "#fcc2ff"],
-    ["pink5", "#f6a6ff"],
-    ["purple1", "#b28dff"],
-    ["purple2", "#c5a3ff"],
-    ["purple3", "#d5aaff"],
-    ["purple4", "#ecd4ff"],
+    ['pink2', '#ff9cee'],
+    ['pink3', '#ffccf9'],
+    ['pink4', '#fcc2ff'],
+    ['pink5', '#f6a6ff'],
+    ['purple1', '#b28dff'],
+    ['purple2', '#c5a3ff'],
+    ['purple3', '#d5aaff'],
+    ['purple4', '#ecd4ff'],
     // ["purple5", "#fb34ff"],
-    ["purple6", "#dcd3ff"],
-    ["purple7", "#a79aff"],
-    ["purple8", "#b5b9ff"],
-    ["purple9", "#97a2ff"],
-    ["bluegreen1", "#afcbff"],
-    ["bluegreen2", "#aff8db"],
-    ["bluegreen3", "#c4faf8"],
-    ["bluegreen4", "#85e3ff"],
-    ["bluegreen5", "#ace7ff"],
+    ['purple6', '#dcd3ff'],
+    ['purple7', '#a79aff'],
+    ['purple8', '#b5b9ff'],
+    ['purple9', '#97a2ff'],
+    ['bluegreen1', '#afcbff'],
+    ['bluegreen2', '#aff8db'],
+    ['bluegreen3', '#c4faf8'],
+    ['bluegreen4', '#85e3ff'],
+    ['bluegreen5', '#ace7ff'],
     // ["bluegreen6", "#6eb5ff"],
-    ["bluegreen7", "#bffcc6"],
-    ["bluegreen8", "#dbffd6"],
-    ["yellow1", "#f3ffe3"],
-    ["yellow2", "#e7ffac"],
-    ["yellow3", "#ffffd1"],
-    ["yellow4", "#fff5ba"],
+    ['bluegreen7', '#bffcc6'],
+    ['bluegreen8', '#dbffd6'],
+    ['yellow1', '#f3ffe3'],
+    ['yellow2', '#e7ffac'],
+    ['yellow3', '#ffffd1'],
+    ['yellow4', '#fff5ba'],
     // ["red1", "#ffc9de"],
-    ["red2", "#ffabab"],
-    ["red3", "#ffbebc"],
-    ["red4", "#ffcbc1"],
-    ["orange1", "#ffd5b3"],
-    ["gray", "#f1efeb"]
+    ['red2', '#ffabab'],
+    ['red3', '#ffbebc'],
+    ['red4', '#ffcbc1'],
+    ['orange1', '#ffd5b3'],
+    ['gray', '#f1efeb'],
 ]);
 
 export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)];
 
 export const DarkPastelSchemaPalette = new Map<string, string>([
-    ["pink2", "#c932b0"],
-    ["purple4", "#913ad6"],
-    ["bluegreen1", "#3978ed"],
-    ["bluegreen7", "#2adb3e"],
-    ["bluegreen5", "#21b0eb"],
-    ["yellow4", "#edcc0c"],
-    ["red2", "#eb3636"],
-    ["orange1", "#f2740f"],
+    ['pink2', '#c932b0'],
+    ['purple4', '#913ad6'],
+    ['bluegreen1', '#3978ed'],
+    ['bluegreen7', '#2adb3e'],
+    ['bluegreen5', '#21b0eb'],
+    ['yellow4', '#edcc0c'],
+    ['red2', '#eb3636'],
+    ['orange1', '#f2740f'],
 ]);
 
 @scriptingGlobal
-@Deserializable("schemaheader")
+@Deserializable('schemaheader')
 export class SchemaHeaderField extends ObjectField {
     @serializable(primitive())
     heading: string;
@@ -69,7 +69,7 @@ export class SchemaHeaderField extends ObjectField {
     @serializable(primitive())
     desc: boolean | undefined; // boolean determines sort order, undefined when no sort
 
-    constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
+    constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
         super();
 
         this.heading = heading;
@@ -111,7 +111,7 @@ export class SchemaHeaderField extends ObjectField {
     }
 
     [Copy]() {
-        return new SchemaHeaderField(this.heading, this.color, this.type);
+        return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed);
     }
 
     [ToScriptString]() {
@@ -120,4 +120,4 @@ export class SchemaHeaderField extends ObjectField {
     [ToString]() {
         return `SchemaHeaderField`;
     }
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3-70-g09d2


From d8f2a45a95a14151d3484c3c14f72d217a818786 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 22 Aug 2022 12:50:52 -0400
Subject: made inkMasks animate when they are turned on /off.    fixed
 inkStrokes to be interpolated when they have an activeFrame set.

---
 src/client/util/CurrentUserUtils.ts                |  4 ++--
 src/client/views/DocumentDecorations.tsx           |  4 ++--
 src/client/views/InkStrokeProperties.ts            |  1 -
 src/client/views/InkingStroke.tsx                  | 22 +++++++++++++++----
 .../CollectionFreeFormLayoutEngines.tsx            |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  9 ++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++++----
 src/client/views/nodes/DocumentView.tsx            |  1 -
 src/client/views/nodes/button/FontIconBox.tsx      | 25 +++++++++++-----------
 9 files changed, 47 insertions(+), 30 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f7d072d80..20c57c617 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -633,7 +633,7 @@ export class CurrentUserUtils {
             { title: "Left",   toolTip: "Left align",   btnType: ButtonType.ToggleButton, icon: "align-left",   scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
             { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
             { title: "Right",  toolTip: "Right align",  btnType: ButtonType.ToggleButton, icon: "align-right",  scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
-            { title: "NoLink", toolTip: "Auto Link",    btnType: ButtonType.ToggleButton, icon: "link",         scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
+            { title: "NoLink", toolTip: "Auto Link",    btnType: ButtonType.ToggleButton, icon: "link",         scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
             { title: "Dictate",toolTip: "Dictate",      btnType: ButtonType.ToggleButton, icon: "microphone",   scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
         ];
     }
@@ -676,7 +676,7 @@ export class CurrentUserUtils {
                     CollectionViewType.Grid, CollectionViewType.NoteTaking]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon: "",           toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20,  scripts: {}},
+            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20,  scripts: {}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6d1397395..ab77af0f4 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -203,8 +203,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 if (this._deleteAfterIconify) {
                     views.forEach(iconView => {
                         Doc.setNativeView(iconView.props.Document);
-                        if (iconView.props.Document.isInkMask && iconView.props.Document.activeFrame !== undefined) {
-                            iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMaks to be "turned off" without removing them from the collection which allows them to function properly in a presenation.
+                        if (iconView.props.Document.activeFrame) {
+                            iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
                         } else {
                             iconView.props.removeDocument?.(iconView.props.Document);
                         }
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index b32c9d54c..1f5f16592 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -68,7 +68,6 @@ export class InkStrokeProperties {
                                 doc.x = oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale;
                                 doc.y = oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale;
                                 if (doc.activeFrame !== undefined) {
-                                    doc.data = ComputedField.MakeInterpolated('data', 'activeFrame', doc, NumCast(doc.activeFrame));
                                     const findexed = Cast(doc[`data-indexed`], listSpec(InkField), []).slice();
                                     findexed[NumCast(doc.activeFrame)] = new InkField(newPoints);
                                     doc[`data-indexed`] = new List<InkField>(findexed);
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 52efbdfd7..520d40abf 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,7 +25,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
-import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, FieldValue, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
@@ -45,6 +45,9 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { INK_MASK_SIZE } from './global/globalCssVariables.scss';
 import './InkStroke.scss';
 import Color = require('color');
+import { ComputedField } from '../../fields/ScriptField';
+import { listSpec } from '../../fields/Schema';
+import { List } from '../../fields/List';
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -56,7 +59,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
     }
     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;
+    private _disposers: { [key: string]: IReactionDisposer } = {};
 
     @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 "
@@ -64,13 +67,24 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
 
     componentDidMount() {
         this.props.setContentView?.(this);
-        this._selDisposer = reaction(
+        this._disposers.activeFrame = reaction(
+            () => this.rootDoc.activeFrame !== undefined && !(ComputedField.WithoutComputed(() => FieldValue(this.rootDoc[this.fieldKey])) instanceof ComputedField),
+            () => {
+                const newPoints = Cast(this.rootDoc[this.fieldKey], InkField, null).inkData;
+                this.rootDoc[this.fieldKey] = ComputedField.MakeInterpolated(this.fieldKey, 'activeFrame', this.rootDoc, NumCast(this.rootDoc.activeFrame));
+                const findexed = Cast(this.rootDoc[`data-indexed`], listSpec(InkField), []).slice();
+                findexed[NumCast(this.rootDoc.activeFrame)] = new InkField(newPoints);
+                this.rootDoc[this.fieldKey + '-indexed'] = new List<InkField>(findexed);
+            },
+            { fireImmediately: true }
+        );
+        this._disposers.selfDisper = reaction(
             () => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
             selected => !selected && (InkStrokeProperties.Instance._controlButton = false)
         );
     }
     componentWillUnmount() {
-        this._selDisposer?.();
+        Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
 
     // transform is the inherited screentolocal xf plus any scaling that was done to make the stroke
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index ee01c341b..b210e7d9a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -45,7 +45,7 @@ export interface PoolData {
 export interface ViewDefResult {
     ele: JSX.Element;
     bounds?: ViewDefBounds;
-    inkMask?: boolean;
+    inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer;   -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden
 }
 function toLabel(target: FieldResult<Field>) {
     if (typeof target === 'number' || Number(target)) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 010132aa5..d80fcdfc3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -20,15 +20,20 @@
     pointer-events: none;
 }
 
+.collectionfreeformview-mask-empty,
 .collectionfreeformview-mask {
-    mix-blend-mode: multiply;
     z-index: 5000;
     width: $INK_MASK_SIZE;
     height: $INK_MASK_SIZE;
     transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF);
-    background-color: rgba(0, 0, 0, 0.7);
     pointer-events: none;
     position: absolute;
+    background-color: transparent;
+    transition: background-color 1s ease 0s;
+}
+.collectionfreeformview-mask {
+    mix-blend-mode: multiply;
+    background-color: rgba(0, 0, 0, 0.7);
 }
 
 .collectionfreeformview-viewdef {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0fd326091..1d518076a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -119,9 +119,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable ChildDrag: DocumentView | undefined; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
 
     @computed get views() {
-        const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask).map(ele => ele.ele);
-        const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && !ele.inkMask).map(ele => ele.ele);
-        if (viewsMask.length) renderableEles.push(<div className="collectionfreeformview-mask">{viewsMask}</div>);
+        const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele);
+        const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele);
+        if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>);
         return renderableEles;
     }
     @computed get fitToContentVals() {
@@ -251,7 +251,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     isCurrent(doc: Doc) {
-        if (doc.isInkMask && doc.opacity === 0) return false; // bcz: hacky --- allows inkMasks to be "turned off" in a presentation without removing them from the collection.  otherwise, they still render a gray background.. need to come back to this and fix.
         const dispTime = NumCast(doc._timecodeToShow, -1);
         const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5);
         const curTime = NumCast(this.Document._currentTimecode, -1);
@@ -1465,7 +1464,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 elements.push({
                     ele: this.getChildDocView(entry[1]),
                     bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica),
-                    inkMask: BoolCast(entry[1].pair.layout.isInkMask),
+                    inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1,
                 })
             );
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 172adcafe..74143a731 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1656,7 +1656,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         ref={this.ContentRef}
                         style={{
                             transition: this.props.dataTransition,
-                            //position: this.props.Document.isInkMask ? 'absolute' : undefined,
                             transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
                             width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
                             height:
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index cb68c1ac3..fc5bf86f4 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -90,9 +90,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     @computed get label() {
         return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
     }
-    @computed get icon() {
-        return StrCast(this.dataDoc.icon, 'user') as any;
-    }
+    Icon = (color: string) => {
+        const icon = StrCast(this.dataDoc.icon, 'user') as any;
+        const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
+        return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
+    };
     @computed get dropdown() {
         return BoolCast(this.rootDoc.dropDownOpen);
     }
@@ -228,7 +230,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 className={`menuButton ${this.type} ${active}`}
                 style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
                 onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
-                <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                {this.Icon(color)}
                 {!this.label || !FontIconBox.GetShowLabels() ? null : (
                     <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
                         {' '}
@@ -384,7 +386,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
                 onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))}
                 onPointerDown={e => e.stopPropagation()}>
-                <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                {this.Icon(color)}
                 <div className="colorButton-color" style={{ backgroundColor: curColor }} />
                 {label}
                 {/* {dropdownCaret} */}
@@ -436,7 +438,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         } else {
             return (
                 <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
-                    <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                    {this.Icon(color)}
                     {label}
                 </div>
             );
@@ -453,7 +455,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         return (
             <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
                 <div className="menuButton-wrap">
-                    <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={'black'} size={'sm'} />
+                    {this.Icon(color)}
                     {!this.label || !FontIconBox.GetShowLabels() ? null : (
                         <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
                             {' '}
@@ -506,7 +508,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.TextButton:
                 button = (
                     <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
-                        <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                        {this.Icon(color)}
                         {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
                         {label}
                     </div>
@@ -531,7 +533,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.ToolButton:
                 button = (
                     <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
-                        <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                        {this.Icon(color)}
                         {label}
                     </div>
                 );
@@ -543,16 +545,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.ClickButton:
                 button = (
                     <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}>
-                        <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+                        {this.Icon(color)}
                         {label}
                     </div>
                 );
                 break;
             case ButtonType.MenuButton:
-                const trailsIcon = <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
                 button = (
                     <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
-                        {this.icon === 'pres-trail' ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />}
+                        {this.Icon(color)}
                         {menuLabel}
                         <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} />
                     </div>
-- 
cgit v1.2.3-70-g09d2


From 2d8f763d763080fd52e940abb1a98b41e2da23fd Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 Sep 2022 14:28:44 -0400
Subject: fixed dragging stackedTimeline entries so that they appear during
 drag.   fixed cursors for video/pdf/stackedTimeline to be a little more
 clear.   fixed initial corner resize of pdfs.  fixed videobox marquee drag
 when viewScale is undefined.

---
 src/client/util/DragManager.ts                       |  9 +++++++--
 src/client/views/DocumentDecorations.tsx             |  3 +++
 src/client/views/MarqueeAnnotator.scss               |  9 ++++-----
 src/client/views/MarqueeAnnotator.tsx                |  3 +--
 .../views/collections/CollectionStackedTimeline.scss |  4 +++-
 .../views/collections/CollectionStackedTimeline.tsx  |  4 +++-
 .../collections/collectionFreeForm/MarqueeView.scss  | 20 +++++++++-----------
 src/client/views/nodes/VideoBox.tsx                  |  4 ++--
 8 files changed, 32 insertions(+), 24 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index cec158d23..664933de0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -357,10 +357,13 @@ export namespace DragManager {
         let rot = 0;
         const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
         const dragElements = eles.map(ele => {
+            let useDim = false;
             if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
                 ele = ele.parentElement.parentElement.parentElement;
                 const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1');
                 if (rotStr) rot = Number(rotStr);
+            } else {
+                useDim = true;
             }
             if (rot < 0) rot += 360;
             if (!ele.parentNode) dragDiv.appendChild(ele);
@@ -402,6 +405,8 @@ export namespace DragManager {
             xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left);
             ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top);
             scalings.push(scaling);
+            const width = useDim ? getComputedStyle(ele).width : '';
+            const height = useDim ? getComputedStyle(ele).height : '';
             Object.assign(dragElement.style, {
                 opacity: '0.7',
                 position: 'absolute',
@@ -414,8 +419,8 @@ export namespace DragManager {
                 borderRadius: getComputedStyle(ele).borderRadius,
                 zIndex: globalCssVariables.contextMenuZindex,
                 transformOrigin: '0 0',
-                width: '',
-                height: '',
+                width,
+                height,
                 transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
             });
             dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3589e014a..a79f727a7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -476,6 +476,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
                 if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                     const doc = Document(docView.rootDoc);
+                    if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) {
+                        doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth;
+                    }
                     const nwidth = docView.nativeWidth;
                     const nheight = docView.nativeHeight;
                     let docheight = doc._height || 0;
diff --git a/src/client/views/MarqueeAnnotator.scss b/src/client/views/MarqueeAnnotator.scss
index c90d48a65..5c65f35e9 100644
--- a/src/client/views/MarqueeAnnotator.scss
+++ b/src/client/views/MarqueeAnnotator.scss
@@ -1,12 +1,11 @@
-
 .marqueeAnnotator-annotationBox {
     position: absolute;
     background-color: rgba(245, 230, 95, 0.616);
 }
 
-
 .marqueeAnnotator-dragBox {
-    position:absolute;
+    position: absolute;
     background-color: transparent;
-    opacity: 0.1;
-}
\ No newline at end of file
+    opacity: 0.2;
+    cursor: default;
+}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index f90ad8bb5..d9a989309 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -271,8 +271,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                     width: `${this._width}px`,
                     height: `${this._height}px`,
                     border: `${this._width === 0 ? '' : '2px dashed black'}`,
-                    opacity: 0.2,
-                }}></div>
+                }}/>
         );
     }
 }
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index aa8502c20..5a107d2ca 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -10,6 +10,7 @@
     border-width: 0 2px 0 2px;
 
     &:hover {
+        cursor: default;
         .collectionStackedTimeline-hover {
             display: block;
         }
@@ -109,14 +110,15 @@
             height: 100%;
             width: 10px;
             pointer-events: all;
-            cursor: ew-resize;
             z-index: 100;
         }
         .collectionStackedTimeline-resizer {
             right: 0;
+            cursor: e-resize;
         }
         .collectionStackedTimeline-left-resizer {
             left: 0;
+            cursor: w-resize;
         }
     }
 
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 2543624d3..b29abf083 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -244,6 +244,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                     if (!wasSelecting) {
                         this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
                         wasSelecting = true;
+                        this._timelineWrapper && (this._timelineWrapper.style.cursor = 'ew-resize');
                     }
                     this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
                     return false;
@@ -260,6 +261,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                         setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
                     }
                     (!isClick || !wasSelecting) && (this._markerEnd = undefined);
+                    this._timelineWrapper && (this._timelineWrapper.style.cursor = '');
                 }),
                 (e, doubleTap) => {
                     if (e.button !== 2) {
@@ -561,7 +563,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
             <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
                 <div
                     className="collectionStackedTimeline-timelineContainer"
-                    style={{ width: this.props.PanelWidth() }}
+                    style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }}
                     onWheel={e => e.stopPropagation()}
                     onScroll={this.setScroll}
                     onMouseMove={e => this.isContentActive() && this.onHover(e)}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 41e4d6b6a..e0f5cbe5b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,16 +1,14 @@
-
 .marqueeView {
     position: inherit;
-    top:0;
-    left:0;
-    width:100%;
-    height:100%;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
     overflow: hidden;
     border-radius: inherit;
     user-select: none;
 }
 
-
 .marqueeView:focus-within {
     overflow: hidden;
 }
@@ -22,13 +20,13 @@
     border-color: black;
     pointer-events: none;
     .marquee-legend {
-        bottom:-18px;
-        left:0;
+        bottom: -18px;
+        left: 0;
         position: absolute;
         font-size: 9;
-        white-space:nowrap;
+        white-space: nowrap;
     }
     .marquee-legend::after {
-        content: "Press <space> for lasso"
+        content: 'Press <space> for lasso';
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index bfb8c1528..5a3594ffc 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -551,7 +551,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return !field ? (
             <div key="loading">Loading</div>
         ) : (
-            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
+            <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'default' }}>
                 <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
                     {this._fullScreen && (
                         <div
@@ -855,7 +855,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // starts marquee selection
     marqueeDown = (e: React.PointerEvent) => {
-        if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
             setupMoveUpEvents(
                 this,
                 e,
-- 
cgit v1.2.3-70-g09d2


From 4315a0378bc54ae9eaa684d416839f635c38e865 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 12 Sep 2022 16:18:42 -0400
Subject: fixed rotating UI for video boxes.  fixed generating error response
 for unsupported video/audio formats.

---
 src/client/views/DocumentDecorations.tsx |  8 +++----
 src/client/views/nodes/VideoBox.tsx      | 38 +++++++++++++++++++++-----------
 src/server/DashUploadUtils.ts            |  3 +++
 3 files changed, 31 insertions(+), 18 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a79f727a7..832d0a35c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -738,11 +738,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 </>
                             )}
 
-                            {useRotation && (
-                                <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                    {'⟲'}
-                                </div>
-                            )}
+                            <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                {'⟲'}
+                            </div>
 
                             {useRounding && (
                                 <div
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 5a3594ffc..b45ee7f6e 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -914,21 +914,33 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     // renders video controls
     componentUI = (boundsLeft: number, boundsTop: number) => {
-        const bounds = this.props.docViewPath().lastElement().getBounds();
-        const left = bounds?.left || 0;
-        const right = bounds?.right || 0;
-        const top = bounds?.top || 0;
-        const height = (bounds?.bottom || 0) - top;
-        const width = Math.max(right - left, 100);
-        const uiHeight = Math.max(25, Math.min(50, height / 10));
-        const uiMargin = Math.min(10, height / 20);
-        const vidHeight = (height * this.heightPercent) / 100;
-        const yPos = top + vidHeight - uiHeight - uiMargin;
-        const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10;
+        const xf = this.props.ScreenToLocalTransform().inverse();
+        const height = this.props.PanelHeight();
+        const vidHeight = (height * this.heightPercent) / 100 / this.scaling();
+        const vidWidth = this.props.PanelWidth() / this.scaling();
+        const uiHeight = 25;
+        const uiMargin = 10;
+        const yBot = xf.transformPoint(0, vidHeight)[1];
+        // prettier-ignore
+        const yMid =  (xf.transformPoint(0, 0)[1] +
+                       xf.transformPoint(0, height / this.scaling())[1]) /  2;
+        const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
+        const xRight = xf.transformPoint(vidWidth, 0)[0];
         const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
-        return this._fullScreen || right - left < 50 ? null : (
+        return this._fullScreen || (xRight - xPos) * 2 < 50 ? null : (
             <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
-                <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}>
+                <div
+                    className="videoBox-ui"
+                    style={{
+                        transformOrigin: 'top left',
+                        transform: `rotate(${NumCast(this.rootDoc.jitterRotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
+                        left: xPos,
+                        top: yMid,
+                        height: uiHeight,
+                        width: (xRight - xPos) * 2 - 20,
+                        transition: this._clicking ? 'top 0.5s' : '',
+                        opacity,
+                    }}>
                     {this.UIButtons}
                 </div>
             </div>
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index e94ef8534..8cf657da4 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -142,6 +142,7 @@ export namespace DashUploadUtils {
                     const result = await UploadImage(path, basename(path));
                     return { source: file, result };
                 }
+                return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${name}). Please convert to an .jpg` } };
             case 'video':
                 if (format.includes('x-matroska')) {
                     console.log('case video');
@@ -179,6 +180,7 @@ export namespace DashUploadUtils {
                 if (videoFormats.includes(format)) {
                     return MoveParsedFile(file, Directory.videos);
                 }
+                return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } };
             case 'application':
                 if (applicationFormats.includes(format)) {
                     return UploadPdf(file);
@@ -191,6 +193,7 @@ export namespace DashUploadUtils {
                 if (audioFormats.includes(format)) {
                     return UploadAudio(file, format);
                 }
+                return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${name}). Please convert to an .mp3` } };
             case 'text':
                 if (types[1] == 'csv') {
                     return UploadCsv(file);
-- 
cgit v1.2.3-70-g09d2


From 65026fdb919132f6046b45fc3b0490df2e5ad2fe Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 14 Sep 2022 11:12:29 -0400
Subject: changed docking view thumbnails to be imagefields, not docs.  allowed
 docking view to be nested to avoid crashes before when they became nested.  
 fixed dropping docs with dropAction of 'alias' to never remove them.  fixed
 clearing startLink to not deselect when link is already started

---
 src/client/util/SelectionManager.ts                |  2 +-
 src/client/views/DashboardView.tsx                 |  4 +--
 src/client/views/DocumentDecorations.tsx           |  2 +-
 .../views/collections/CollectionDockingView.tsx    | 32 ++++++++++++++++------
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/collections/CollectionView.tsx    |  1 +
 src/client/views/collections/TabDocView.tsx        |  2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  4 +--
 src/client/views/nodes/LinkDocPreview.tsx          |  2 +-
 src/client/views/topbar/TopBar.tsx                 |  4 +--
 10 files changed, 36 insertions(+), 19 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 7a555d5f8..a3d6f5227 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -91,7 +91,7 @@ export namespace SelectionManager {
     }
 
     export function Views(): Array<DocumentView> {
-        return Array.from(manager.SelectedViews.keys()).filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+        return Array.from(manager.SelectedViews.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
     }
     export function SelectedSchemaDoc(): Doc | undefined {
         return manager.SelectedSchemaDocument;
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 6415601f1..12fc3133d 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -50,7 +50,7 @@ export class DashboardView extends React.Component {
         this.selectedDashboardGroup = group;
     };
 
-    clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => {
+    clickDashboard = (e: React.MouseEvent, dashboard: Doc) => {
         if (e.detail === 2) {
             Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard);
             Doc.ActiveDashboard = dashboard;
@@ -156,7 +156,7 @@ export class DashboardView extends React.Component {
                     </div>
                     <div className="all-dashboards">
                         {this.getDashboards().map(dashboard => {
-                            const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href;
+                            const href = ImageCast(dashboard.thumb)?.url.href;
                             return (
                                 <div
                                     className="dashboard-container"
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 832d0a35c..0fc33bdea 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -189,7 +189,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
         const views = SelectionManager.Views()
             .slice()
-            .filter(v => v);
+            .filter(v => v && v.props.renderDepth > 0);
         if (forceDeleteOrIconify === false && this._iconifyBatch) return;
         this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
         if (!this._iconifyBatch) {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 10b54edad..31c839af7 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { inheritParentAcls } from '../../../fields/util';
 import { emptyFunction, incrementTitleCopy } from '../../../Utils';
@@ -56,7 +56,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
     constructor(props: SubCollectionViewProps) {
         super(props);
-        runInAction(() => (CollectionDockingView.Instance = this));
+        if (this.props.renderDepth < 0) runInAction(() => (CollectionDockingView.Instance = this));
         //Why is this here?
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
@@ -73,7 +73,7 @@ export class CollectionDockingView extends CollectionSubView() {
     public StartOtherDrag = (e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
         this._flush = this._flush ?? UndoManager.StartBatch('golden layout drag');
         const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) : { type: 'row', content: dragDocs.map(doc => CollectionDockingView.makeDocumentConfig(doc)) };
-        const dragSource = this._goldenLayout.createDragSource(document.createElement('div'), config);
+        const dragSource = CollectionDockingView.Instance?._goldenLayout.createDragSource(document.createElement('div'), config);
         this.tabDragStart(dragSource, finishDrag);
         dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
     };
@@ -395,10 +395,10 @@ export class CollectionDockingView extends CollectionSubView() {
             const _height = Number(getComputedStyle(content).height.replace('px', ''));
             return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
                 const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight });
-                const proto = Cast(img.proto, Doc, null)!;
-                proto['data-nativeWidth'] = _width;
-                proto['data-nativeHeight'] = _height;
-                this.dataDoc.thumb = img;
+                const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
+                proto['thumb-nativeWidth'] = _width;
+                proto['thumb-nativeHeight'] = _height;
+                this.dataDoc.thumb = new ImageField(iconFile);
             });
         }
     }
@@ -532,7 +532,23 @@ export class CollectionDockingView extends CollectionSubView() {
     };
 
     render() {
-        return <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef} />;
+        const href = ImageCast(this.rootDoc.thumb)?.url.href;
+        return this.props.renderDepth > -1 ? (
+            <div>
+                {href ? (
+                    <img
+                        style={{ background: 'white', top: 0, position: 'absolute' }}
+                        src={href} // + '?d=' + (new Date()).getTime()}
+                        width={this.props.PanelWidth()}
+                        height={this.props.PanelHeight()}
+                    />
+                ) : (
+                    <p>nested dashboards has no thumbnail</p>
+                )}
+            </div>
+        ) : (
+            <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef} />
+        );
     }
 }
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4227955ef..7fc1a800b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -212,7 +212,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 let added = false;
                 const dropAction = docDragData.dropAction || docDragData.userDropAction;
                 const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]);
-                const someMoved = !docDragData.userDropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
+                const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
                 if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop));
                 if ((!dropAction || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) {
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 1ee77d4ce..0a06b6e00 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -116,6 +116,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
         switch (type) {
             default:
             case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
+            case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
             case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />;
             case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
             case CollectionViewType.Tree: return <CollectionTreeView key="collview" {...props} />;
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 46386fa71..b9262763f 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -400,7 +400,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap);
 
     @computed get docView() {
-        return !this._activated || !this._document || this._document._viewType === CollectionViewType.Docking ? null : (
+        return !this._activated || !this._document ? null : (
             <>
                 <DocumentView
                     key={this._document[Id]}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index a37de7f69..9ffbf8e37 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Hypothesis } from '../../util/HypothesisUtils';
@@ -287,7 +287,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     </div>
                 ) : null}
                 {this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
-                    <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
+                    <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
                         <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
                     </div>
                 ) : null}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 93ca22d5d..f50524e4e 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -249,7 +249,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                             searchFilterDocs={returnEmptyDoclist}
                             ContainingCollectionDoc={undefined}
                             ContainingCollectionView={undefined}
-                            renderDepth={-1}
+                            renderDepth={0}
                             suppressSetHeight={true}
                             PanelWidth={this.width}
                             PanelHeight={this.height}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index cbcfed06f..096b2561f 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -28,12 +28,12 @@ import { ReportManager } from '../../util/ReportManager';
 @observer
 export class TopBar extends React.Component {
     navigateToHome = () => {
-        CollectionDockingView.Instance?.CaptureThumbnail()?.then(() => {
+        (CollectionDockingView.Instance?.CaptureThumbnail() ?? new Promise<void>(res => res())).then(() => {
             Doc.ActivePage = 'home';
             DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use
         });
     };
-    
+
     render() {
         const activeDashboard = Doc.ActiveDashboard;
         return (
-- 
cgit v1.2.3-70-g09d2


From 37be2477782b0b372c16d46df3f9634ebf3677fe Mon Sep 17 00:00:00 2001
From: Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com>
Date: Wed, 21 Sep 2022 01:43:03 -0400
Subject: restyled document decorations

and changed novice mode to exclude `data` in treeView
---
 src/client/views/DocumentButtonBar.tsx             |   2 +-
 src/client/views/DocumentDecorations.scss          | 189 ++++++++++++++-------
 src/client/views/DocumentDecorations.tsx           |  13 +-
 src/client/views/_nodeModuleOverrides.scss         |   4 +-
 .../views/collections/CollectionDockingView.scss   |   8 +
 .../views/collections/CollectionTreeView.scss      |   1 +
 src/client/views/collections/TabDocView.tsx        |   4 +-
 src/client/views/collections/TreeView.scss         |  20 ++-
 src/client/views/collections/TreeView.tsx          |   4 +-
 src/client/views/global/globalCssVariables.scss    |   4 +
 src/client/views/nodes/button/FontIconBox.tsx      |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          |  16 +-
 src/client/views/topbar/TopBar.tsx                 |   2 +-
 13 files changed, 178 insertions(+), 91 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 35c0b9a7d..0bfe6e87a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -434,7 +434,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
                     </div>
                 ) : null}
-                <div className="documentButtonBar-button">{this.recordButton}</div>
+                {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.recordButton}</div>}
                 {
                     Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
                     /*<div className="documentButtonBar-button">
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index b490278c3..2e8d31478 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -22,6 +22,129 @@ $resizeHandler: 8px;
     grid-template-columns: $resizeHandler 1fr $resizeHandler;
     pointer-events: none;
 
+    .documentDecorations-topbar {
+        display: flex;
+        grid-column-start: 1;
+        grid-column-end: 4;
+        flex-direction: row;
+        gap: 2px;
+
+
+        .documentDecorations-openButton {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #3ce312;
+            border: solid 1.5px #0a620a;
+            color: #3ce312;
+            transition: 0.1s ease;
+            font-size: 11px;
+            opacity: 1;
+            pointer-events: all;
+            width: 20px;
+            height: 20px;
+            min-width: 20px;
+            border-radius: 100%;
+            cursor: pointer;
+
+            &:hover {
+                color: #02600d;
+            }
+        }
+        
+        .documentDecorations-closeButton {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #fb9d75;
+            border: solid 1.5px #a94442;
+            color: #fb9d75;
+            transition: 0.1s ease;
+            opacity: 1;
+            pointer-events: all;
+            width: 20px;
+            height: 20px;
+            min-width: 20px;
+            border-radius: 100%;
+            cursor: pointer;
+
+            &:hover {
+                color: #a94442;
+            }
+        
+            > svg {
+                margin: 0;
+            }
+        }
+
+        .documentDecorations-minimizeButton {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #ffdd00;
+            border: solid 1.5px #a94442;
+            color: #ffdd00;
+            transition: 0.1s ease;
+            font-size: 11px;
+            opacity: 1;
+            grid-column: 2;
+            pointer-events: all;
+            width: 20px;
+            height: 20px;
+            min-width: 20px;
+            border-radius: 100%;
+            cursor: pointer;
+
+            &:hover {
+                color: #a94442;
+            }
+        
+            > svg {
+                margin: 0;
+            }
+        }
+
+        .documentDecorations-title-Dark,
+        .documentDecorations-title {
+            opacity: 1;
+            width: calc(100% - 60px); // = margin-left + margin-right
+            grid-column: 3;
+            pointer-events: auto;
+            overflow: hidden;
+            text-align: center;
+            display: flex;
+            height: 20px;
+            border-radius: 8px;
+            outline: none;
+            border: none;
+
+            .documentDecorations-titleSpan,
+            .documentDecorations-titleSpan-Dark {
+                width: 100%;
+                border-radius: 8px;
+                background: $light-gray;
+                display: inline-block;
+                cursor: move;
+            }
+            .documentDecorations-titleSpan-Dark {
+                background: hsla(0, 0%, 0%, 0.412);
+            }
+        }
+
+        .documentDecorations-title-Dark {
+            color: white;
+            background: black;
+        }
+
+        .documentDecorations-titleBackground {
+            background: $light-gray;
+            border-radius: 8px;
+            width: 100%;
+            height: 100%;
+            position: absolute;
+        }
+    }
+
     .documentDecorations-centerCont {
         grid-column: 2;
         background: none;
@@ -225,76 +348,12 @@ $resizeHandler: 8px;
         cursor: ew-resize;
     }
 
-    .documentDecorations-title-Dark,
-    .documentDecorations-title {
-        opacity: 1;
-        width: calc(100% - 8px); // = margin-left + margin-right
-        grid-column: 2;
-        grid-column-end: 2;
-        pointer-events: auto;
-        overflow: hidden;
-        text-align: center;
-        display: flex;
-        margin-left: 6px; // closeButton width (14) - leftColumn width (8)
-        margin-right: 2px;
-        height: 20px;
-        position: absolute;
-        border-radius: 8px;
-        background: rgba(159, 159, 159, 0.1);
-
-        .documentDecorations-titleSpan,
-        .documentDecorations-titleSpan-Dark {
-            width: 100%;
-            border-radius: 8px;
-            background: #ffffffa0;
-            position: absolute;
-            display: inline-block;
-            cursor: move;
-        }
-        .documentDecorations-titleSpan-Dark {
-            background: hsla(0, 0%, 0%, 0.412);
-        }
-    }
-    .documentDecorations-title-Dark {
-        color: white;
-        background: black;
-    }
-
-    .documentDecorations-titleBackground {
-        background: #ffffffcf;
-        border-radius: 8px;
-        width: 100%;
-        height: 100%;
-        position: absolute;
-    }
-
     .focus-visible {
         margin-left: 0px;
     }
 }
 
-.documentDecorations-openButton {
-    display: flex;
-    align-items: center;
-    opacity: 1;
-    grid-column-start: 3;
-    pointer-events: all;
-    cursor: pointer;
-}
 
-.documentDecorations-closeButton {
-    display: flex;
-    align-items: center;
-    opacity: 1;
-    grid-column: 1;
-    pointer-events: all;
-    width: 14px;
-    cursor: pointer;
-
-    > svg {
-        margin: 0;
-    }
-}
 
 .documentDecorations-background {
     background: lightblue;
@@ -329,7 +388,7 @@ $resizeHandler: 8px;
     justify-content: center;
     align-items: center;
     gap: 5px;
-    background: $medium-gray;
+    background: $light-gray;
 }
 
 .linkButtonWrapper {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 0fc33bdea..95025bf70 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -719,9 +719,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                 height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
                             }}>
-                            {hideDeleteButton ? <div /> : topBtn('close', this.hasIcons ? 'times' : 'window-maximize', undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), 'Close')}
-                            {titleArea}
-                            {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
+                            <div className="documentDecorations-topbar">
+                                {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
+                                {hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
+                                {titleArea}
+                                {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
+                            </div>
                             {hideResizers ? null : (
                                 <>
                                     <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
@@ -738,9 +741,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 </>
                             )}
 
-                            <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                            {!Doc.noviceMode && <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
                                 {'⟲'}
-                            </div>
+                            </div>}
 
                             {useRounding && (
                                 <div
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
index b1cce8705..c99281323 100644
--- a/src/client/views/_nodeModuleOverrides.scss
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -44,8 +44,8 @@ div .lm_header {
     position: absolute;
     width: calc(100% - 60px);
     overflow: scroll;
-    background: #6b6b6b6b; //$dark-gray;
-    border-radius: 5px;
+    background: transparent; //$dark-gray;
+    border-radius: 0px;
 }
 
 .lm_tab {
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 091ba8e74..ac3541f2c 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -154,7 +154,15 @@
         background: $white;
     }
 
+    .lm_controls {
+        height: 27px;
+        display: flex;
+        align-content: center;
+        justify-content: center;
+    }
+
     .lm_controls > li {
+        height: 27px !important;
         opacity: 1;
         transform: scale(1);
     }
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index c0561e42c..a182a72c5 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -104,6 +104,7 @@
     text-overflow: ellipsis;
     white-space: pre-wrap;
     min-width: 10px;
+    grid-column: 2;
 }
 .docContainer-system {
     font-variant: all-small-caps;
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index a82bd2dc8..98121f423 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -56,7 +56,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         return this._document && Doc.Layout(this._document);
     }
     @computed get tabColor() {
-        return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor)));
+        let tabColor = StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor)));
+        if (tabColor === 'transparent') return 'black';
+        return tabColor;
     }
     @computed get tabTextColor() {
         return this._document?.type === DocumentType.PRES ? 'black' : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color)));
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index ce87e6f89..cfb97709b 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,13 +53,16 @@
     }
 
     .bullet {
+        grid-column: 1;
+        display: flex;
+        justify-content: center;
+        align-items: center;
         position: relative;
         width: $TREE_BULLET_WIDTH;
+        min-height: 20px;
         color: $medium-gray;
-        margin-top: 3px;
-        // transform: scale(1.3, 1.3); // bcz: why was this here?  It makes displaying images in the treeView for documents that have icons harder.
         border: #80808030 1px solid;
-        border-radius: 4px;
+        border-radius: 5px;
     }
 }
 
@@ -113,7 +116,15 @@
 .treeView-header-editing,
 .treeView-header {
     border: transparent 1px solid;
-    display: flex;
+    display: grid;
+    align-items: center;
+    grid-auto-columns: 22px auto 22px;
+    width: 100%;
+    border-radius: 5px;
+
+    &:hover {
+        background-color: #bdddf5;
+    }
 
     //align-items: center;
     ::-webkit-scrollbar {
@@ -140,6 +151,7 @@
 
     .treeView-rightButtons {
         display: flex;
+        grid-column: 3;
         align-items: center;
         margin-left: 0.25rem;
         opacity: 0.75;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 833e364b3..b489b5214 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -736,7 +736,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                         }}
                     />
                 )}
-                {this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
+                {Doc.noviceMode ? null : this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
                     <span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
                         {this.treeViewExpandedView}
                     </span>
@@ -953,7 +953,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 <div
                     className={`treeView-header` + (editing ? '-editing' : '')}
                     key="titleheader"
-                    style={{ width: StrCast(this.doc.treeViewHeaderWidth, 'max-content') }}
+                    style={{ width: '100%' }}
                     ref={this._header}
                     onClick={this.ignoreEvent}
                     onPointerDown={this.ignoreEvent}
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 8a75aeef8..ac2c04aaa 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -7,6 +7,10 @@ $medium-gray: #9f9f9f;
 $dark-gray: #323232;
 $black: #000000;
 
+$close-red: red;
+$minimize-yellow: yellow;
+$open-green: green;
+
 $light-blue: #bdddf5;
 $light-blue-transparent: #bdddf590;
 $medium-blue: #4476f7;
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 4a6099fb3..6d1751b25 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -275,7 +275,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                     icon = 'globe-asia';
                     text = 'User Default';
                 }
-                noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
+                noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
             } else if (script?.script.originalScript.startsWith('setFont')) {
                 const editorView = RichTextMenu.Instance?.TextView?.EditorView;
                 text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index ab59e6112..7f0f13437 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -2321,8 +2321,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
         return (
-            <div className="presBox-buttons" style={{ background: Doc.ActivePresentation === this.rootDoc ? 'green' : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
-                {isMini ? null : (
+            <div className="presBox-buttons" style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
+                {isMini || Doc.noviceMode ? null : (
                     <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
                         <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
                             List
@@ -2330,11 +2330,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <option onPointerDown={StopEvent} value={CollectionViewType.Tree}>
                             Tree
                         </option>
-                        {Doc.noviceMode ? null : (
-                            <option onPointerDown={StopEvent} value={CollectionViewType.Carousel3D}>
-                                3D Carousel
-                            </option>
-                        )}
+                        <option onPointerDown={StopEvent} value={CollectionViewType.Carousel3D}>
+                            3D Carousel
+                        </option>
                     </select>
                 )}
                 <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}>
@@ -2653,14 +2651,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         ) : null}
                     </div>
 
-                    {
+                    {/* {
                         // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack
                         <Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}>
                             <button className="add-slide-button" onPointerDown={this.startMarqueeCreateSlide}>
                                 + NEW SLIDE
                             </button>
                         </Tooltip>
-                    }
+                    } */}
                 </div>
             </div>
         );
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index bbdb4621e..2fe0c6e79 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -52,7 +52,7 @@ export class TopBar extends React.Component {
                     <span style={{color: Colors.LIGHT_GRAY, fontWeight: 200}}>brown</span><span style={{color: Colors.LIGHT_BLUE, fontWeight: 500}}>dash</span>
                 </div>
                 }
-                {Doc.ActiveDashboard && <Button
+                {Doc.ActiveDashboard && !Doc.noviceMode && <Button
                     text="Explore"
                     tooltip="Browsing mode for directly navigating to documents"
                     fontSize={FontSize.SECONDARY}
-- 
cgit v1.2.3-70-g09d2


From 87aaeef9b9be2a1faab1cc7a7cf35792d3c01252 Mon Sep 17 00:00:00 2001
From: Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com>
Date: Wed, 21 Sep 2022 13:49:51 -0400
Subject: added resize back in

---
 src/client/views/DocumentDecorations.tsx | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 95025bf70..797730c10 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -29,6 +29,8 @@ import { DocumentView } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
+import { IconButton } from 'browndash-components';
+import { FaUndo } from 'react-icons/fa';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -719,12 +721,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                 height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
                             }}>
-                            <div className="documentDecorations-topbar">
+                            {hideResizers ? null : (<div className="documentDecorations-topbar">
                                 {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                 {hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
                                 {titleArea}
                                 {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
-                            </div>
+                            </div>)}
                             {hideResizers ? null : (
                                 <>
                                     <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
@@ -741,8 +743,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 </>
                             )}
 
-                            {!Doc.noviceMode && <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                {'⟲'}
+                            {useRotation && <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                <IconButton icon={<FaUndo/>} isCircle={true} hoverStyle={"lighten"} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY}/>
                             </div>}
 
                             {useRounding && (
-- 
cgit v1.2.3-70-g09d2


From 0bf2c263c82eb08a8f9545967c5907fc00be9a9f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 22 Sep 2022 11:32:38 -0400
Subject: fixed presenting backward in presBox.  fixed context menu to appear
 above other widgets

---
 src/client/views/ContextMenu.scss               |  2 +-
 src/client/views/DocumentDecorations.tsx        | 25 +++++++++++++++----------
 src/client/views/global/globalCssVariables.scss |  2 +-
 src/client/views/nodes/trails/PresBox.tsx       | 25 +++++++++++++++----------
 4 files changed, 32 insertions(+), 22 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 1e6a377de..cbe14060a 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -3,7 +3,7 @@
 .contextMenu-cont {
     position: absolute;
     display: flex;
-    z-index: 100000;
+    z-index: $contextMenu-zindex;
     box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
     flex-direction: column;
     background: whitesmoke;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 797730c10..4675571c2 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -31,6 +31,7 @@ import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
 import { IconButton } from 'browndash-components';
 import { FaUndo } from 'react-icons/fa';
+import { VideoBox } from './nodes/VideoBox';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -678,7 +679,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
         // Rotation constants: Only allow rotation on ink and images
-        const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox;
+        const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox || seldoc.ComponentView instanceof VideoBox;
         const rotation = NumCast(seldoc.rootDoc._jitterRotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
@@ -721,12 +722,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                 height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
                             }}>
-                            {hideResizers ? null : (<div className="documentDecorations-topbar">
-                                {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
-                                {hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
-                                {titleArea}
-                                {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
-                            </div>)}
+                            {hideResizers ? null : (
+                                <div className="documentDecorations-topbar">
+                                    {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
+                                    {hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
+                                    {titleArea}
+                                    {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
+                                </div>
+                            )}
                             {hideResizers ? null : (
                                 <>
                                     <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
@@ -743,9 +746,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 </>
                             )}
 
-                            {useRotation && <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                <IconButton icon={<FaUndo/>} isCircle={true} hoverStyle={"lighten"} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY}/>
-                            </div>}
+                            {useRotation && (
+                                <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                    <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
+                                </div>
+                            )}
 
                             {useRounding && (
                                 <div
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index ac2c04aaa..e68f9abe3 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -47,7 +47,7 @@ $topbar-height: 37px;
 $antimodemenu-height: 36px;
 
 // dragged items
-$contextMenu-zindex: 100000; // context menu shows up over everything
+$contextMenu-zindex: 100002; // context menu shows up over everything
 $radialMenu-zindex: 100000; // context menu shows up over everything
 
 // borders
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 042c55a46..69f817d79 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -234,8 +234,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
     // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
     // No more frames in current doc and next slide is defined, therefore move to next slide
-    nextSlide = (activeNext: Doc) => {
-        let nextSelected = this.itemIndex + 1;
+    nextSlide = (slideNum?: number) => {
+        let nextSelected = slideNum ?? this.itemIndex + 1;
         this.gotoDocument(nextSelected, this.activeItem);
         for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
             if (this.childDocs[nextSelected].groupWithUp) {
@@ -261,10 +261,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.nextInternalFrame(targetDoc, activeItem);
         } else if (this.childDocs[this.itemIndex + 1] !== undefined) {
             // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide
-            this.nextSlide(activeNext);
+            const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]);
+            const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex;
+            this.nextSlide(curLast + 1);
         } else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
             // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode
-            this.gotoDocument(0, this.activeItem);
+            this.nextSlide(0);
         }
     };
 
@@ -280,8 +282,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         let prevSelected = this.itemIndex;
         // Functionality for group with up
         let didZoom = activeItem.presMovement;
-        for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) {
-            didZoom = this.childDocs[prevSelected].presMovement;
+        for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) {
+            didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom;
         }
         if (lastFrame !== undefined && curFrame >= 1) {
             // Case 1: There are still other frames and should go through all frames before going to previous slide
@@ -289,7 +291,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
             // Case 2: There are no other frames so it should go to the previous slide
             prevSelected = Math.max(0, prevSelected - 1);
-            this.gotoDocument(prevSelected, activeItem);
+            this.nextSlide(prevSelected);
+            this.rootDoc._itemIndex = prevSelected;
             if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame);
         } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
             // Case 3: Pres loop is on so it should go to the last slide
@@ -2458,12 +2461,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 <div
                     className="presPanel-button"
                     style={{ opacity: presStart ? 0.4 : 1 }}
-                    onClick={() => {
+                    onClick={e => {
                         this.back();
                         if (this._presTimer) {
                             clearTimeout(this._presTimer);
                             this.layoutDoc.presStatus = PresStatus.Manual;
                         }
+                        e.stopPropagation();
                     }}>
                     <FontAwesomeIcon icon={'arrow-left'} />
                 </div>
@@ -2475,18 +2479,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 <div
                     className="presPanel-button"
                     style={{ opacity: presEnd ? 0.4 : 1 }}
-                    onClick={() => {
+                    onClick={e => {
                         this.next();
                         if (this._presTimer) {
                             clearTimeout(this._presTimer);
                             this.layoutDoc.presStatus = PresStatus.Manual;
                         }
+                        e.stopPropagation();
                     }}>
                     <FontAwesomeIcon icon={'arrow-right'} />
                 </div>
                 <div className="presPanel-divider"></div>
                 <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
-                    <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}>
+                    <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.nextSlide(0)}>
                         <b>1</b>
                     </div>
                 </Tooltip>
-- 
cgit v1.2.3-70-g09d2


From 2de2e154912d95a15e5f890c9d1a3a7a11610107 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Sep 2022 20:44:33 -0400
Subject: a bunch of changes to trails: link to trail to initiate trail when
 following link.  cleanly separate pin layout data vs. content data.

---
 src/client/documents/Documents.ts                  |   3 +
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/util/DocumentManager.ts                 |   2 +-
 src/client/util/type_decls.d                       |   3 +
 src/client/views/DocumentButtonBar.scss            |  22 ++-
 src/client/views/DocumentButtonBar.tsx             |  71 +++++++---
 src/client/views/DocumentDecorations.tsx           |   3 +-
 src/client/views/MainView.tsx                      |   1 +
 src/client/views/collections/TabDocView.tsx        |   2 +-
 src/client/views/collections/TreeView.tsx          |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +-
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |  42 +++---
 .../collections/collectionFreeForm/MarqueeView.tsx |  12 +-
 .../collectionLinear/CollectionLinearView.tsx      |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 src/client/views/nodes/ScriptingBox.tsx            |   5 +
 src/client/views/nodes/trails/PresBox.tsx          | 147 +++++++++------------
 src/client/views/nodes/trails/PresElementBox.tsx   |  66 ++++++---
 18 files changed, 225 insertions(+), 170 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fb68fba38..4ca9972f8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1398,6 +1398,9 @@ export namespace DocUtils {
                         heading: Doc.name,
                         checked: 'boolean',
                         containingTreeView: Doc.name,
+                        altKey: 'boolean',
+                        ctrlKey: 'boolean',
+                        shiftKey: 'boolean',
                     });
                 }
             });
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index fdac3c00a..6f9975be4 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -657,7 +657,7 @@ export class CurrentUserUtils {
                     CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
                     CollectionViewType.Grid, CollectionViewType.NoteTaking]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
-            { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_)'}},
+            { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
             { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20,  scripts: {}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 50190061a..9a46d20de 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -244,7 +244,7 @@ export class DocumentManager {
                             res(ViewAdjustment.doNothing);
                         }),
                 });
-            if (focusView.props.Document.layoutKey === 'layout_icon') {
+            if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) {
                 focusView.iconify(() => doFocus(true));
             } else {
                 doFocus(false);
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 9063dc894..1a93bbe59 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -67,6 +67,9 @@ interface RegExp {
     readonly sticky: boolean;
     readonly unicode: boolean;
 }
+interface Date {
+    now() : string;
+}
 interface String {
     codePointAt(pos: number): number | undefined;
     includes(searchString: string, position?: number): boolean;
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index a112f4745..1e93ba5e2 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -1,6 +1,6 @@
-@import "global/globalCssVariables";
+@import 'global/globalCssVariables';
 
-$linkGap : 3px;
+$linkGap: 3px;
 
 .documentButtonBar-linkFlyout {
     grid-column: 2/4;
@@ -18,6 +18,21 @@ $linkGap : 3px;
     cursor: pointer;
 }
 
+.documentButtonBar-pinTypes {
+    position: absolute;
+    display: flex;
+    width: 60px;
+    top: -14px;
+    background: black;
+    height: 20px;
+    align-items: center;
+}
+.documentButtonBar-pinIcon {
+    &:hover {
+        background-color: lightblue;
+    }
+}
+
 .documentButtonBar-linkButton-empty,
 .documentButtonBar-linkButton-nonempty {
     height: 20px;
@@ -99,7 +114,6 @@ $linkGap : 3px;
     transform: scale(1.05);
 }
 
-
 @-moz-keyframes spin {
     100% {
         -moz-transform: rotate(360deg);
@@ -127,4 +141,4 @@ $linkGap : 3px;
     100% {
         box-shadow: 0 0 0 10px rgba(0, 255, 0, 0);
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 0bfe6e87a..d42ff436f 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -5,8 +5,8 @@ import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
-import { Cast, DocCast, NumCast } from '../../fields/Types';
-import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { Cast, NumCast } from '../../fields/Types';
+import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
 import { Docs } from '../documents/Documents';
@@ -26,7 +26,6 @@ import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
 import { TemplateMenu } from './TemplateMenu';
 import React = require('react');
-import { DocumentType } from '../documents/DocumentTypes';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -159,12 +158,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         })();
 
         return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? null : (
-            <Tooltip
-                title={
-                    <>
-                        <div className="dash-tooltip">{title}</div>
-                    </>
-                }>
+            <Tooltip title={<div className="dash-tooltip">{title}</div>}>
                 <div
                     className="documentButtonBar-button"
                     style={{ backgroundColor: this.pullColor }}
@@ -202,14 +196,12 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         size="sm"
                         style={{ WebkitAnimation: animation, MozAnimation: animation }}
                         icon={(() => {
+                            // prettier-ignore
                             switch (this.openHover) {
                                 default:
-                                case UtilityButtonState.Default:
-                                    return dataDoc.googleDocUnchanged === false ? (this.pullIcon as any) : fetch;
-                                case UtilityButtonState.OpenRight:
-                                    return 'arrow-alt-circle-right';
-                                case UtilityButtonState.OpenExternally:
-                                    return 'share';
+                                case UtilityButtonState.Default: return dataDoc.googleDocUnchanged === false ? (this.pullIcon as any) : fetch;
+                                case UtilityButtonState.OpenRight: return 'arrow-alt-circle-right';
+                                case UtilityButtonState.OpenExternally: return 'share';
                             }
                         })()}
                     />
@@ -231,21 +223,66 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             </Tooltip>
         );
     }
+    @observable expandPin = false;
+    @observable subPin = '';
     @computed
     get pinButton() {
         const targetDoc = this.view0?.props.Document;
+        const pinBtn = (pinDocLayout: boolean, pinDocContent: boolean, icon: IconProp) => {
+            const tooltip = `Pin Document and Save ${this.subPin} to trail`;
+            return !tooltip ? null : (
+                <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
+                    <div className="documentButtonBar-pinIcon">
+                        <FontAwesomeIcon
+                            className="documentdecorations-icon"
+                            style={{ width: 20 }}
+                            key={icon.toString()}
+                            size="sm"
+                            icon={icon}
+                            onPointerEnter={action(
+                                e =>
+                                    (this.subPin =
+                                        (pinDocLayout ? 'Layout' : '') +
+                                        (pinDocLayout && pinDocContent ? ' &' : '') +
+                                        (pinDocContent ? ' Content View' : '') +
+                                        (pinDocLayout && pinDocContent ? '(shift+alt)' : pinDocLayout ? '(shift)' : pinDocContent ? '(alt)' : ''))
+                            )}
+                            onPointerLeave={action(e => (this.subPin = ''))}
+                            onClick={e => {
+                                const docs = this.props
+                                    .views()
+                                    .filter(v => v)
+                                    .map(dv => dv!.rootDoc);
+                                TabDocView.PinDoc(docs, { pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                                e.stopPropagation();
+                            }}
+                        />
+                    </div>
+                </Tooltip>
+            );
+        };
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{SelectionManager.Views().length > 1 ? 'Pin multiple documents to trail (use shift to pin with view)' : 'Pin to trail (use shift to pin with view)'}</div>}>
+            <Tooltip title={<div className="dash-tooltip">{`Pin Document ${SelectionManager.Views().length > 1 ? 'multiple documents' : ''} to Trail`}</div>}>
                 <div
                     className="documentButtonBar-icon"
                     style={{ color: 'white' }}
+                    onPointerEnter={action(e => (this.expandPin = true))}
+                    onPointerLeave={action(e => (this.expandPin = false))}
                     onClick={e => {
                         const docs = this.props
                             .views()
                             .filter(v => v)
                             .map(dv => dv!.rootDoc);
-                        TabDocView.PinDoc(docs, { pinDocView: e.shiftKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                        TabDocView.PinDoc(docs, { pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                        e.stopPropagation();
                     }}>
+                    {this.expandPin ? (
+                        <div className="documentButtonBar-pinTypes">
+                            {pinBtn(true, false, 'window-maximize')}
+                            {pinBtn(false, true, 'address-card')}
+                            {pinBtn(true, true, 'id-card')}
+                        </div>
+                    ) : null}
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4675571c2..3e0de6d15 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -678,8 +678,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
-        // Rotation constants: Only allow rotation on ink and images
-        const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox || seldoc.ComponentView instanceof VideoBox;
+        const useRotation = true; // when do we want an object to not rotate?
         const rotation = NumCast(seldoc.rootDoc._jitterRotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 857a5522c..350883f70 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -303,6 +303,7 @@ export class MainView extends React.Component {
                 fa.faStop,
                 fa.faCalculator,
                 fa.faWindowMaximize,
+                fa.faIdCard,
                 fa.faAddressCard,
                 fa.faQuestionCircle,
                 fa.faArrowLeft,
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index b1f57f3fd..c4da70371 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -233,7 +233,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             pinDoc.presentationTargetDoc = doc;
             pinDoc.title = doc.title + ' - Slide';
             pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children.  the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
-            pinDoc.presMovement = pinProps?.pinDocView && !pinProps?.pinWithView ? PresMovement.None : PresMovement.Zoom;
+            pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
             pinDoc.groupWithUp = false;
             pinDoc.context = curPres;
             // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements.  That way the PresBox could configure all of its children at render time
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index c34a6faaa..640984ad6 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -782,7 +782,7 @@ export class TreeView extends React.Component<TreeViewProps> {
 
     onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeViewChildDoubleClick, !this.props.treeView.outlineMode ? this._openScript?.() : null);
 
-    refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document);
+    refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document, {});
     ignoreEvent = (e: any) => {
         if (this.props.isContentActive(true)) {
             e.stopPropagation();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7b6944e1e..8824c2b01 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1719,7 +1719,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             event: () => (this.Document._fitContentsToBox = !this.fitContentsToBox),
             icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt',
         });
-        appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinDocView: true, panelWidth: this.props.PanelWidth(), panelHeight: this.props.PanelHeight() }), icon: 'map-pin' });
+        appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' });
         //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
         this.props.ContainingCollectionView && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
@@ -2249,6 +2249,6 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
-ScriptingGlobals.add(function pinWithView(readOnly: boolean) {
-    !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocView: true, panelWidth: view.props.PanelWidth(), panelHeight: view.props.PanelHeight() }));
+ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) {
+    !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }));
 });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 8a8b528f6..488f51d77 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -1,11 +1,11 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { observer } from "mobx-react";
-import { unimplementedFunction } from "../../../../Utils";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu";
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
+import { observer } from 'mobx-react';
+import { unimplementedFunction } from '../../../../Utils';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 
 @observer
 export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -25,44 +25,34 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     render() {
-        const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 19, transform: 'translate(-2px, -2px)' }} />;
+        const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 19, transform: 'translate(-2px, -2px)' }} />;
         const buttons = [
             <Tooltip key="collect" title={<div className="dash-tooltip">Create a Collection</div>} placement="bottom">
-                <button
-                    className="antimodeMenu-button"
-                    onPointerDown={this.createCollection}>
+                <button className="antimodeMenu-button" onPointerDown={this.createCollection}>
                     <FontAwesomeIcon icon="object-group" size="lg" />
                 </button>
             </Tooltip>,
             <Tooltip key="group" title={<div className="dash-tooltip">Create a Grouping</div>} placement="bottom">
-                <button
-                    className="antimodeMenu-button"
-                    onPointerDown={e => this.createCollection(e, true)}>
+                <button className="antimodeMenu-button" onPointerDown={e => this.createCollection(e, true)}>
                     <FontAwesomeIcon icon="layer-group" size="lg" />
                 </button>
             </Tooltip>,
             <Tooltip key="summarize" title={<div className="dash-tooltip">Summarize Documents</div>} placement="bottom">
-                <button
-                    className="antimodeMenu-button"
-                    onPointerDown={this.summarize}>
+                <button className="antimodeMenu-button" onPointerDown={this.summarize}>
                     <FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
                 </button>
             </Tooltip>,
             <Tooltip key="delete" title={<div className="dash-tooltip">Delete Documents</div>} placement="bottom">
-                <button
-                    className="antimodeMenu-button"
-                    onPointerDown={this.delete}>
+                <button className="antimodeMenu-button" onPointerDown={this.delete}>
                     <FontAwesomeIcon icon="trash-alt" size="lg" />
                 </button>
             </Tooltip>,
-            <Tooltip key="pinWithView" title={<div className="dash-tooltip">Pin with selected region</div>} placement="bottom">
-                <button
-                    className="antimodeMenu-button"
-                    onPointerDown={this.pinWithView}>
+            <Tooltip key="pinWithView" title={<div className="dash-tooltip">Pin selected region to trail</div>} placement="bottom">
+                <button className="antimodeMenu-button" onPointerDown={this.pinWithView}>
                     {presPinWithViewIcon}
                 </button>
             </Tooltip>,
         ];
         return this.getElement(buttons);
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 584c9690f..a020b67cd 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -18,7 +18,7 @@ import { Transform } from '../../../util/Transform';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PinViewProps, PresBox } from '../../nodes/trails/PresBox';
+import { PresBox } from '../../nodes/trails/PresBox';
 import { VideoBox } from '../../nodes/VideoBox';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { PreviewCursor } from '../../PreviewCursor';
@@ -61,6 +61,11 @@ export interface MarqueeViewBounds {
 
 @observer
 export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> {
+    public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) {
+        const ps = NumCast(pinDoc._viewScale, 1);
+        return { left: NumCast(pinDoc._panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
+    }
+
     private _commandExecuted = false;
     @observable _lastX: number = 0;
     @observable _lastY: number = 0;
@@ -426,10 +431,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     @action
     pinWithView = async () => {
         const doc = this.props.Document;
-        const viewOptions: PinViewProps = {
-            bounds: this.Bounds,
-        };
-        TabDocView.PinDoc(doc, { pinWithView: viewOptions, pinDocView: true });
+        TabDocView.PinDoc(doc, { pinViewport: this.Bounds });
         MarqueeOptionsMenu.Instance.fadeOut(true);
         this.hideMarquee();
     };
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index f6eb2fce4..92f6bbb64 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -208,7 +208,7 @@ export class CollectionLinearView extends CollectionSubView() {
                         checked={BoolCast(this.props.Document.linearViewIsExpanded)}
                         ref={this.addMenuToggle}
                         onChange={action(e => {
-                            ScriptCast(this.Document.onClick).script.run({
+                            ScriptCast(this.Document.onClick)?.script.run({
                                 this: this.layoutDoc,
                                 self: this.rootDoc,
                                 _readOnly_: false,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ba061ce8f..913d5a7ef 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -577,6 +577,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                                 documentView: this.props.DocumentView(),
                                 clientX,
                                 clientY,
+                                altKey,
                                 shiftKey,
                             },
                             console.log
@@ -589,7 +590,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 }
             } else if (this.onClickHandler?.script && !isScriptBox()) {
                 // bcz: hack? don't execute script if you're clicking on a scripting box itself
-                const { clientX, clientY, shiftKey } = e;
+                const { clientX, clientY, shiftKey, altKey } = e;
                 const func = () =>
                     this.onClickHandler.script.run(
                         {
@@ -602,6 +603,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                             clientX,
                             clientY,
                             shiftKey,
+                            altKey,
                         },
                         console.log
                     ).result?.select === true
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 1c9b0bc0e..37afcdd8b 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -115,6 +115,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
 
     @action
     componentDidMount() {
+        this.props.setContentView?.(this);
         this.rawText = this.rawScript;
         const observer = new _global.ResizeObserver(
             action((entries: any) => {
@@ -480,6 +481,10 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
         return value;
     }
 
+    scrollFocus = () => {
+        return undefined;
+    };
+
     getSuggestedParams(pos: number) {
         const firstScript = this.rawText.slice(0, pos);
         const indexP = firstScript.lastIndexOf('.');
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0c4d514cd..18441aace 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -31,19 +31,16 @@ import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentVie
 import { FieldView, FieldViewProps } from '../FieldView';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
+import { privateEncrypt } from 'crypto';
+import { ScriptingBox } from '../ScriptingBox';
 
 export interface PinProps {
     audioRange?: boolean;
     activeFrame?: number;
     hidePresBox?: boolean;
-    pinWithView?: PinViewProps;
-    pinDocView?: boolean; // whether the current view specs of the document should be saved the pinned document
-    panelWidth?: number; // panel width and height of the document (used to compute the bounds of the pinned view area)
-    panelHeight?: number;
-}
-
-export interface PinViewProps {
-    bounds: MarqueeViewBounds;
+    pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
+    pinDocLayout?: boolean; // pin layout info (width/height/x/y)
+    pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text)
 }
 
 @observer
@@ -268,6 +265,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode
             this.nextSlide(0);
         }
+        return this.itemIndex;
     };
 
     // Called when the user activates 'back' - to move to the previous part of the pres. trail
@@ -298,6 +296,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // Case 3: Pres loop is on so it should go to the last slide
             this.gotoDocument(this.childDocs.length - 1, activeItem);
         }
+        return this.itemIndex;
     };
 
     //The function that is called when a document is clicked or reached through next or back.
@@ -351,7 +350,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
         const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
         const clippable = [DocumentType.COMPARISON].includes(target.type as any);
-        const dataview = [DocumentType.INK].includes(target.type as any) && target.activeFrame === undefined;
+        const dataview = [DocumentType.INK, DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
         const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
         return { scrollable, pannable, temporal, clippable, dataview, textview };
     }
@@ -383,7 +382,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const dv = DocumentManager.Instance.getDocumentView(bestTarget);
                 if (dv) {
                     const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
-                    activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presZoom !== undefined ? computedScale : Math.min(computedScale, NumCast(bestTarget._viewScale)));
+                    activeItem.presMovement === 'zoom' && (bestTarget._viewScale = computedScale);
                     dv.ComponentView?.brushView?.(viewport);
                 }
             } else {
@@ -400,47 +399,40 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     /// target doc when navigating to it.
     @action
     static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) {
-        if (pinProps?.pinWithView) {
-            // If pinWithView option set then update scale and x / y props of slide
-            const bounds = pinProps.pinWithView.bounds;
-            pinDoc.presPinView = true;
-            pinDoc.presPinViewX = bounds.left + bounds.width / 2;
-            pinDoc.presPinViewY = bounds.top + bounds.height / 2;
-            pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
-        }
-        if (pinProps?.pinDocView) {
-            const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(pinDoc);
-            pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable || dataview || textview || pinProps.activeFrame !== undefined;
+        const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(pinDoc);
+        if (pinProps?.pinDocLayout) {
+            pinDoc.presPinLayout = true;
             pinDoc.presX = NumCast(targetDoc.x);
             pinDoc.presY = NumCast(targetDoc.y);
             pinDoc.presRot = NumCast(targetDoc.jitterRotation);
             pinDoc.presWidth = NumCast(targetDoc.width);
             pinDoc.presHeight = NumCast(targetDoc.height);
-
-            if (scrollable) {
-                pinDoc.presPinViewScroll = pinDoc._scrollTop;
-            }
+        }
+        if (pinProps?.pinDocContent) {
+            pinDoc.presPinData = scrollable || temporal || pannable || clippable || dataview || textview || pinProps.activeFrame !== undefined;
+            if (textview) pinDoc.presData = targetDoc.text instanceof ObjectField ? targetDoc.text[Copy]() : targetDoc.text;
+            if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
             if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
+            if (dataview) pinDoc.presData = targetDoc.data instanceof ObjectField ? targetDoc.data[Copy]() : targetDoc.data;
+            if (pannable) {
+                pinDoc.presPinViewX = NumCast(pinDoc._panX);
+                pinDoc.presPinViewY = NumCast(pinDoc._panY);
+                pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1);
+            }
             if (temporal) {
                 pinDoc.presStartTime = pinDoc._currentTimecode;
                 const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(pinDoc.presStartTime) + 0.1);
                 pinDoc.presEndTime = NumCast(pinDoc.clipEnd, duration);
             }
-            if (textview) pinDoc.presData = targetDoc.text instanceof ObjectField ? targetDoc.text[Copy]() : targetDoc.text;
-            if (dataview) pinDoc.presData = targetDoc.data instanceof ObjectField ? targetDoc.data[Copy]() : targetDoc.data;
-            if (pannable || scrollable) {
-                const panX = NumCast(pinDoc._panX);
-                const panY = NumCast(pinDoc._panY);
-                const pw = NumCast(pinProps.panelWidth);
-                const ph = NumCast(pinProps.panelHeight);
-                const ps = NumCast(pinDoc._viewScale, 1);
-                if (pw && ph && ps) {
-                    pinDoc.presPinViewBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]);
-                }
-                pinDoc.presPinViewX = panX;
-                pinDoc.presPinViewY = panY;
-                pinDoc.presPinViewScale = ps;
-            }
+        }
+        if (pinProps?.pinViewport) {
+            // If pinWithView option set then update scale and x / y props of slide
+            const bounds = pinProps.pinViewport;
+            pinDoc.presPinView = true;
+            pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1);
+            pinDoc.presPinViewX = bounds.left + bounds.width / 2;
+            pinDoc.presPinViewY = bounds.top + bounds.height / 2;
+            pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
         }
     }
 
@@ -497,7 +489,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
-        if (activeItem.presPinView && DocCast(targetDoc.context)?._currentFrame === undefined) {
+        if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) {
             const transTime = NumCast(activeItem.presTransition, 500);
             const presTransitionTime = `all ${transTime}ms`;
             targetDoc._dataTransition = presTransitionTime;
@@ -514,11 +506,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
             LightboxView.SetLightboxDoc(undefined);
             const zooming = activeItem.presMovement !== PresMovement.Pan;
-            DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom));
+            DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom, 1));
+        } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
+            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
         }
         // After navigating to the document, if it is added as a presPinView then it will
         // adjust the pan and scale to that of the pinView when it was added.
-        if (activeItem.presPinView) {
+        if (activeItem.presPinData || activeItem.presPinView) {
             clearTimeout(PresBox._navTimer);
             // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
             const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
@@ -615,42 +609,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc
     @action
     startAutoPres = (startSlide: number) => {
-        this.updateCurrentPresentation();
-        let activeItem: Doc = this.activeItem;
-        let targetDoc: Doc = this.targetDoc;
-        let duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition);
-        const timer = (ms: number) => new Promise(res => (this._presTimer = setTimeout(res, ms)));
-        const load = async () => {
-            // Wrap the loop into an async function for this to work
-            for (var i = startSlide; i < this.childDocs.length; i++) {
-                activeItem = Cast(this.childDocs[this.itemIndex], Doc, null);
-                targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null);
-                duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition);
-                if (duration < 100) {
-                    duration = 2500;
-                }
-                if (NumCast(targetDoc.lastFrame) > 0) {
-                    for (var f = 0; f < NumCast(targetDoc.lastFrame); f++) {
-                        await timer(duration / NumCast(targetDoc.lastFrame));
-                        this.next();
-                    }
-                }
-
-                await timer(duration);
-                this.next(); // then the created Promise can be awaited
-                if (i === this.childDocs.length - 1) {
-                    setTimeout(() => {
-                        clearTimeout(this._presTimer);
-                        if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = PresStatus.Manual;
-                        else if (this.layoutDoc.presLoop) this.startAutoPres(0);
-                    }, duration);
-                }
-            }
-        };
         this.layoutDoc.presStatus = PresStatus.Autoplay;
         this.startPresentation(startSlide);
-        this.gotoDocument(startSlide, activeItem);
-        load();
+        clearTimeout(this._presTimer);
+        const func = (itemIndex: number) => {
+            if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual;
+            else
+                this._presTimer = setTimeout(
+                    () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex),
+                    NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)
+                );
+        };
+
+        func(this.itemIndex);
     };
 
     // The function pauses the auto presentation
@@ -659,7 +630,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (this.layoutDoc.presStatus === PresStatus.Autoplay) {
             if (this._presTimer) clearTimeout(this._presTimer);
             this.layoutDoc.presStatus = PresStatus.Manual;
-            this.layoutDoc.presLoop = false;
             this.childDocs.forEach(this.stopTempMedia);
         }
     };
@@ -667,7 +637,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //The function that resets the presentation by removing every action done by it. It also
     //stops the presentaton.
     resetPresentation = () => {
-        this.rootDoc._itemIndex = 0;
         this.childDocs
             .map(doc => Cast(doc.presentationTargetDoc, Doc, null))
             .filter(doc => doc instanceof Doc)
@@ -706,8 +675,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * @param startIndex: index that the presentation will start at
      */
     startPresentation = (startIndex: number) => {
-        this.updateCurrentPresentation();
-        this.childDocs.map(doc => {
+        this.childDocs.forEach(doc => {
             const tagDoc = doc.presentationTargetDoc as Doc;
             if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) {
                 tagDoc.opacity = 0;
@@ -716,6 +684,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 tagDoc.opacity = 0;
             }
         });
+        this.gotoDocument(startIndex, this.activeItem);
     };
 
     /**
@@ -996,7 +965,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 break;
             case 'Spacebar':
             case ' ':
-                if (this.layoutDoc.presStatus === PresStatus.Manual) this.startAutoPres(this.itemIndex);
+                if (this.layoutDoc.presStatus === PresStatus.Manual) this.startOrPause(true);
                 else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer);
                 handled = true;
                 break;
@@ -1254,7 +1223,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (activeItem && targetDoc) {
             const type = targetDoc.type;
             const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
-            const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75;
+            const zoom = NumCast(activeItem.presZoom, 1) * 100;
             let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
             if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
             const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None';
@@ -1808,6 +1777,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
+    scrollFocus = () => {
+        this.startOrPause(false);
+        return undefined;
+    };
+
     // Case in which the document has keyframes to navigate to next key frame
     @action
     nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => {
@@ -2480,7 +2454,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     <FontAwesomeIcon icon={'arrow-left'} />
                 </div>
                 <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
-                    <div className="presPanel-button" onClick={this.startOrPause}>
+                    <div className="presPanel-button" onClick={e => this.startOrPause(true)}>
                         <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} />
                     </div>
                 </Tooltip>
@@ -2529,7 +2503,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @action
-    startOrPause = () => {
+    startOrPause = (makeActive = true) => {
+        makeActive && this.updateCurrentPresentation();
         if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
         else this.pauseAutoPres();
     };
@@ -2613,7 +2588,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <FontAwesomeIcon icon={'arrow-left'} />
                     </div>
                     <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
-                        <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.startOrPause, false, false)}>
+                        <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}>
                             <FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} />
                         </div>
                     </Tooltip>
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index e6d08cd53..fe2668492 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -26,6 +26,7 @@ import { PresMovement } from './PresEnums';
 import React = require('react');
 import { InkField } from '../../../../fields/InkField';
 import { RichTextField } from '../../../../fields/RichTextField';
+import { MarqueeView } from '../../collections/collectionFreeForm';
 /**
  * This class models the view a document added to presentation will have in the presentation.
  * It involves some functionality for its buttons and options.
@@ -142,7 +143,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     this.presExpandDocumentClick();
                 }}>
                 <div className="presItem-groupNum">{`${ind + 1}.`}</div>
-                {/* style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }} */}
                 <div className="presItem-name">
                     <EditableView
                         ref={this._titleRef}
@@ -304,7 +304,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @undoBatch
     @action
-    updateView = (targetDoc: Doc, activeItem: Doc) => {
+    updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => {
+        activeItem.presX = NumCast(targetDoc.x);
+        activeItem.presY = NumCast(targetDoc.y);
+        activeItem.presRot = NumCast(targetDoc.jitterRotation);
+        activeItem.presWidth = NumCast(targetDoc.width);
+        activeItem.presHeight = NumCast(targetDoc.height);
+    };
+    /**
+     * Method called for updating the view of the currently selected document
+     *
+     * @param targetDoc
+     * @param activeItem
+     */
+    @undoBatch
+    @action
+    updateCapturedViewContents = (targetDoc: Doc, activeItem: Doc) => {
         switch (targetDoc.type) {
             case DocumentType.PDF:
             case DocumentType.WEB:
@@ -326,20 +341,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const clipWidth = targetDoc._clipWidth;
                 activeItem.presPinClipWidth = clipWidth;
                 break;
+            case DocumentType.COL:
             default:
-                const x = targetDoc._panX;
-                const y = targetDoc._panY;
-                const scale = targetDoc._viewScale;
-                activeItem.presPinViewX = x;
-                activeItem.presPinViewY = y;
-                activeItem.presPinViewScale = scale;
+                const bestView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+                if (activeItem.presPinViewBounds && bestView) {
+                    const bounds = MarqueeView.CurViewBounds(targetDoc, bestView.props.PanelWidth(), bestView.props.PanelHeight());
+                    activeItem.presPinView = true;
+                    activeItem.presPinViewScale = NumCast(targetDoc._viewScale, 1);
+                    activeItem.presPinViewX = bounds.left + bounds.width / 2;
+                    activeItem.presPinViewY = bounds.top + bounds.height / 2;
+                    activeItem.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
+                } else {
+                    activeItem.presPinViewX = targetDoc._panX;
+                    activeItem.presPinViewY = targetDoc._panY;
+                    activeItem.presPinViewScale = targetDoc._viewScale;
+                }
         }
-
-        activeItem.presX = NumCast(targetDoc.x);
-        activeItem.presY = NumCast(targetDoc.y);
-        activeItem.presRot = NumCast(targetDoc.jitterRotation);
-        activeItem.presWidth = NumCast(targetDoc.width);
-        activeItem.presHeight = NumCast(targetDoc.height);
     };
 
     @computed get recordingIsInOverlay() {
@@ -506,16 +523,23 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */}
                         <div className="presItem-slideButtons">
-                            <Tooltip title={<div className="dash-tooltip">Update view</div>}>
-                                <div className="slideButton" onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? 'flex' : 'none' }}>
-                                    V
+                            <Tooltip title={<div className="dash-tooltip">Update captured doc layout</div>}>
+                                <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinLayout ? 'flex' : 'none' }}>
+                                    L
                                 </div>
                             </Tooltip>
-                            {!Doc.noviceMode && <Tooltip title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
-                                <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
-                                    <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
+                            <Tooltip title={<div className="dash-tooltip">Update captured doc content</div>}>
+                                <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinData || activeItem.presPinView ? 'flex' : 'none' }}>
+                                    C
                                 </div>
-                            </Tooltip>}
+                            </Tooltip>
+                            {!Doc.noviceMode && (
+                                <Tooltip title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
+                                    <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
+                                        <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
+                                    </div>
+                                </Tooltip>
+                            )}
                             <Tooltip title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
                                 <div
                                     className="slideButton"
-- 
cgit v1.2.3-70-g09d2


From 8e8cca45755426e2921e2de78556ad60d79a98a8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 4 Oct 2022 11:54:55 -0400
Subject: fix to show close and open icons of hideResizers documents. fixes for
 native size control of text and freeform collections.  fixes for lightbox
 view of native size collections.   fix for 'false' script.

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/views/DocumentDecorations.tsx           | 24 ++++++++++------------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 ++++-
 src/fields/Doc.ts                                  |  6 ++++++
 src/fields/ScriptField.ts                          |  2 +-
 5 files changed, 23 insertions(+), 15 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fb68fba38..cd647f5e8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -396,6 +396,7 @@ export namespace Docs {
                         _yMargin: 10,
                         nativeDimModifiable: true,
                         treeViewGrowsHorizontally: true,
+                        nativeHeightUnfrozen: true,
                         forceReflow: true,
                         links: '@links(self)',
                     },
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4675571c2..d5db45494 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,8 +1,10 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
+import { IconButton } from 'browndash-components';
 import { action, computed, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
+import { FaUndo } from 'react-icons/fa';
 import { DateField } from '../../fields/DateField';
 import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc';
 import { Document } from '../../fields/documentSchemas';
@@ -28,10 +30,8 @@ import { LightboxView } from './LightboxView';
 import { DocumentView } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
-import React = require('react');
-import { IconButton } from 'browndash-components';
-import { FaUndo } from 'react-icons/fa';
 import { VideoBox } from './nodes/VideoBox';
+import React = require('react');
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -503,7 +503,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     }
                     let actualdW = Math.max(width + dW * scale, 20);
                     let actualdH = Math.max(height + dH * scale, 20);
-                    const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen);
+                    const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
                     if (fixedAspect) {
                         if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
                             if (dragRight && modifyNativeDim) {
@@ -534,7 +534,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             } else doc._height = actualdH;
                         }
                     } else {
-                        const maxHeight = 0; //Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
+                        const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
                         dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
                         dW && (doc._width = actualdW);
                         dH && (doc._autoHeight = false);
@@ -722,14 +722,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                 height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
                             }}>
-                            {hideResizers ? null : (
-                                <div className="documentDecorations-topbar">
-                                    {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
-                                    {hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
-                                    {titleArea}
-                                    {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
-                                </div>
-                            )}
+                            <div className="documentDecorations-topbar">
+                                {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
+                                {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
+                                {hideResizers ? <div /> : titleArea}
+                                {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
+                            </div>
                             {hideResizers ? null : (
                                 <>
                                     <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7b6944e1e..93bf143df 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -156,7 +156,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
     @computed get cachedCenteringShiftY(): number {
         const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
-        return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+        // if freeform has a native aspect, then the panel height needs to be adjusted to match it
+        const aspect = this.props.DocumentView?.().nativeHeight ? this.props.DocumentView?.().nativeHeight / this.props.DocumentView?.().nativeWidth : 1;
+        return this.props.isAnnotationOverlay ? 0 : (aspect * this.props.PanelHeight()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedGetLocalTransform(): Transform {
         return Transform.Identity()
@@ -190,6 +192,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     elementFunc = () => this._layoutElements;
     shrinkWrap = () => {
+        if (this.props.DocumentView?.().nativeWidth) return;
         const vals = this.fitToContentVals;
         this.layoutDoc._panX = vals.bounds.cx;
         this.layoutDoc._panY = vals.bounds.cy;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ea3c122a4..0ceaff968 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1402,11 +1402,17 @@ export namespace Doc {
                 layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
                 layoutDoc._nativeWidth = undefined;
                 layoutDoc._nativeHeight = undefined;
+                layoutDoc._forceReflow = undefined;
+                layoutDoc._nativeHeightUnfrozen = undefined;
+                layoutDoc._nativeDimModifiable = undefined;
             } else {
                 layoutDoc._autoHeight = false;
                 if (!Doc.NativeWidth(layoutDoc)) {
                     layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
                     layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
+                    layoutDoc._forceReflow = true;
+                    layoutDoc._nativeHeightUnfrozen = true;
+                    layoutDoc._nativeDimModifiable = true;
                 }
             }
         });
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 3cb50a4c0..4896c027d 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -104,7 +104,7 @@ export class ScriptField extends ObjectField {
         }
         this.rawscript = rawscript;
         this.setterscript = setterscript;
-        this.script = script ?? (CompileScript('false') as CompiledScript);
+        this.script = script ?? (CompileScript('false', { addReturn: true }) as CompiledScript);
     }
 
     //     init(callback: (res: Field) => any) {
-- 
cgit v1.2.3-70-g09d2


From 0e41cb323c486b23c70e53d14a563adaf0eeef9e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 7 Oct 2022 15:03:16 -0400
Subject: fixes for equations : :eq as option to ctrl-m inside a text box. 
 added background for equations.  fixed cursor focus issues.

---
 src/client/views/DocumentDecorations.tsx           |  30 ++---
 src/client/views/nodes/ComparisonBox.tsx           | 142 ++++++++++++---------
 src/client/views/nodes/EquationBox.tsx             |   4 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |  38 ++++--
 .../views/nodes/formattedText/EquationView.tsx     |  18 ++-
 .../formattedText/ProsemirrorExampleTransfer.ts    |   9 +-
 .../views/nodes/formattedText/RichTextRules.ts     |   9 ++
 src/client/views/nodes/formattedText/nodes_rts.ts  |  26 ++--
 8 files changed, 177 insertions(+), 99 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 1d6075606..8b8c32642 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -605,26 +605,26 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
     render() {
         const bounds = this.Bounds;
-        const seldoc = SelectionManager.Views().slice(-1)[0];
-        if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+        const seldocview = SelectionManager.Views().slice(-1)[0];
+        if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
             return null;
         }
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
-        const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup || this._isRounding || this._isRotating;
-        const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
-        const hideDocumentButtonBar = seldoc.props.hideDocumentButtonBar || seldoc.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
+        const hideResizers = seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
+        const hideTitle = seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
+        const hideDocumentButtonBar = seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
         // if multiple documents have been opened at the same time, then don't show open button
         const hideOpenButton =
-            seldoc.props.hideOpenButton ||
-            seldoc.rootDoc.hideOpenButton ||
+            seldocview.props.hideOpenButton ||
+            seldocview.rootDoc.hideOpenButton ||
             SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
         const hideDeleteButton =
             this._isRounding ||
             this._isRotating ||
-            seldoc.props.hideDeleteButton ||
-            seldoc.rootDoc.hideDeleteButton ||
+            seldocview.props.hideDeleteButton ||
+            seldocview.rootDoc.hideDeleteButton ||
             SelectionManager.Views().some(docView => {
                 const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
                 return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin);
@@ -678,15 +678,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
-        const useRotation = true; // when do we want an object to not rotate?
-        const rotation = NumCast(seldoc.rootDoc._jitterRotation);
+        const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
+        const rotation = NumCast(seldocview.rootDoc._jitterRotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
 
         // Radius constants
-        const useRounding = seldoc.ComponentView instanceof ImageBox || seldoc.ComponentView instanceof FormattedTextBox;
-        const borderRadius = numberValue(StrCast(seldoc.rootDoc.borderRounding));
-        const docMax = Math.min(NumCast(seldoc.rootDoc.width) / 2, NumCast(seldoc.rootDoc.height) / 2);
+        const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox;
+        const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding));
+        const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2);
         const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
         const radiusHandle = (borderRadius / docMax) * maxDist;
         const radiusHandleLocation = Math.min(radiusHandle, maxDist);
@@ -739,7 +739,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     <div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
                                     <div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
 
-                                    {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')}
+                                    {seldocview.props.renderDepth <= 1 || !seldocview.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')}
                                 </>
                             )}
 
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 5ea6d567a..d74da9748 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, observable } from 'mobx';
-import { observer } from "mobx-react";
+import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
@@ -9,19 +9,20 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
-import "./ComparisonBox.scss";
+import './ComparisonBox.scss';
 import { DocumentView, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
-import React = require("react");
-
+import React = require('react');
 
 @observer
 export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); }
-    protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(ComparisonBox, fieldKey);
+    }
+    protected _multiTouchDisposer?: import('../../util/InteractionUtils').InteractionUtils.MultiTouchEventDisposer | undefined;
     private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined];
 
-    @observable _animating = "";
+    @observable _animating = '';
 
     protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
         this._disposers[disposerId]?.();
@@ -29,7 +30,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
             // create disposers identified by disposerId to remove drag & drop listeners
             this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc);
         }
-    }
+    };
 
     @undoBatch
     private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
@@ -40,88 +41,113 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                 this.dataDoc[fieldKey] = droppedDocs[0];
             }
         }
-    }
+    };
 
     private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
-        e.button !== 2 && setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => {
-            // on click, animate slider movement to the targetWidth
-            this._animating = "all 200ms";
-            this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth();
-            setTimeout(action(() => this._animating = ""), 200);
-        }), false);
-    }
+        e.button !== 2 &&
+            setupMoveUpEvents(
+                this,
+                e,
+                this.onPointerMove,
+                emptyFunction,
+                action(() => {
+                    // on click, animate slider movement to the targetWidth
+                    this._animating = 'all 200ms';
+                    this.layoutDoc._clipWidth = (targetWidth * 100) / this.props.PanelWidth();
+                    setTimeout(
+                        action(() => (this._animating = '')),
+                        200
+                    );
+                }),
+                false
+            );
+    };
 
     @action
     private onPointerMove = ({ movementX }: PointerEvent) => {
-        const width = movementX * this.props.ScreenToLocalTransform().Scale + NumCast(this.layoutDoc._clipWidth) / 100 * this.props.PanelWidth();
+        const width = movementX * this.props.ScreenToLocalTransform().Scale + (NumCast(this.layoutDoc._clipWidth) / 100) * this.props.PanelWidth();
         if (width && width > 5 && width < this.props.PanelWidth()) {
-            this.layoutDoc._clipWidth = width * 100 / this.props.PanelWidth();
+            this.layoutDoc._clipWidth = (width * 100) / this.props.PanelWidth();
         }
         return false;
-    }
+    };
 
     @undoBatch
     clearDoc = (e: React.MouseEvent, fieldKey: string) => {
         e.stopPropagation; // prevent click event action (slider movement) in registerSliding
         delete this.dataDoc[fieldKey];
-    }
+    };
 
     docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
-        if (property === StyleProp.PointerEvents) return "none";
+        if (property === StyleProp.PointerEvents) return 'none';
         return this.props.styleProvider?.(doc, props, property);
-    }
+    };
 
     render() {
-        const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%";
+        const clipWidth = NumCast(this.layoutDoc._clipWidth) + '%';
         const clearButton = (which: string) => {
-            return <div className={`clear-button ${which}`}
-                onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding 
-                onClick={e => this.clearDoc(e, which)}>
-                <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" />
-            </div>;
+            return (
+                <div
+                    className={`clear-button ${which}`}
+                    onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding
+                    onClick={e => this.clearDoc(e, which)}>
+                    <FontAwesomeIcon className={`clear-button ${which}`} icon={'times'} size="sm" />
+                </div>
+            );
         };
         const displayDoc = (which: string) => {
             const whichDoc = Cast(this.dataDoc[which], Doc, null);
             // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null);
             const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc;
-            return whichDoc ? <>
-                <DocumentView
-                    ref={(r) => {
-                        whichDoc !== targetDoc && r?.focus(whichDoc);
-                    }}
-                    {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
-                    isContentActive={returnFalse}
-                    isDocumentActive={returnFalse}
-                    styleProvider={this.docStyleProvider}
-                    Document={targetDoc}
-                    DataDoc={undefined}
-                    hideLinkButton={true}
-                    pointerEvents={returnNone} />
-                {clearButton(which)}
-            </> :  // placeholder image if doc is missing
+            return whichDoc ? (
+                <>
+                    <DocumentView
+                        ref={r => {
+                            whichDoc !== targetDoc && r?.focus(whichDoc, {});
+                        }}
+                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                        isContentActive={returnFalse}
+                        isDocumentActive={returnFalse}
+                        styleProvider={this.docStyleProvider}
+                        Document={targetDoc}
+                        DataDoc={undefined}
+                        hideLinkButton={true}
+                        pointerEvents={returnNone}
+                    />
+                    {clearButton(which)}
+                </> // placeholder image if doc is missing
+            ) : (
                 <div className="placeholder">
-                    <FontAwesomeIcon className="upload-icon" icon={"cloud-upload-alt"} size="lg" />
-                </div>;
+                    <FontAwesomeIcon className="upload-icon" icon={'cloud-upload-alt'} size="lg" />
+                </div>
+            );
         };
         const displayBox = (which: string, index: number, cover: number) => {
-            return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }}
-                onPointerDown={e => this.registerSliding(e, cover)}
-                ref={ele => this.createDropTarget(ele, which, index)} >
-                {displayDoc(which)}
-            </div>;
+            return (
+                <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
+                    {displayDoc(which)}
+                </div>
+            );
         };
 
         return (
-            <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}>
-                {displayBox(this.fieldKey === "data" ? "compareBox-after" : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)}
-                <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}>
-                    {displayBox(this.fieldKey === "data" ? "compareBox-before" : `${this.fieldKey}1`, 0, 0)}
+            <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
+                {displayBox(this.fieldKey === 'data' ? 'compareBox-after' : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)}
+                <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
+                    {displayBox(this.fieldKey === 'data' ? 'compareBox-before' : `${this.fieldKey}1`, 0, 0)}
                 </div>
 
-                <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)`, cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? "e-resize" : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? "w-resize" : undefined }}
-                    onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ >
+                <div
+                    className="slide-bar"
+                    style={{
+                        left: `calc(${clipWidth} - 0.5px)`,
+                        cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? 'e-resize' : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined,
+                    }}
+                    onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
+                >
                     <div className="slide-handle" />
                 </div>
-            </div >);
+            </div>
+        );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 0bd30bce9..c279341cc 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -7,6 +7,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { Docs } from '../../documents/Documents';
+import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { LightboxView } from '../LightboxView';
 import './EquationBox.scss';
@@ -45,7 +46,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
             { fireImmediately: true }
         );
     }
-    plot: any;
+
     @action
     keyPressed = (e: KeyboardEvent) => {
         const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', ''));
@@ -76,6 +77,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc);
     };
+    @undoBatch
     onChange = (str: string) => {
         this.dataDoc.text = str;
         const style = this._ref.current && getComputedStyle(this._ref.current.element.current);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 15d0f88f6..e09155ac2 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -4,11 +4,14 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { documentSchema } from '../../../fields/documentSchemas';
+import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
 import { Cast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { Docs } from '../../documents/Documents';
+import { DragManager } from '../../util/DragManager';
+import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
 
@@ -33,7 +36,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this.props.setContentView?.(this);
         reaction(
-            () => [DocListCast(this.dataDoc[this.fieldKey]).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
+            () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
             () => this.createGraph()
         );
     }
@@ -53,8 +56,9 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._plotEle = ele || this._plotEle;
         const width = this.props.PanelWidth();
         const height = this.props.PanelHeight();
-        const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)');
+        const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'));
         try {
+            this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
             this._plot = functionPlot({
                 target: '#' + this._plotEle.id,
                 width,
@@ -62,17 +66,34 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) },
                 yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) },
                 grid: true,
-                data: [
-                    {
-                        fn,
-                        // derivative: { fn: "2 * x", updateOnMouseMove: true }
-                    },
-                ],
+                data: fns.map(fn => ({
+                    fn,
+                    // derivative: { fn: "2 * x", updateOnMouseMove: true }
+                })),
             });
         } catch (e) {
             console.log(e);
         }
     };
+
+    @undoBatch
+    drop = (e: Event, de: DragManager.DropEvent) => {
+        if (de.complete.docDragData?.droppedDocuments.length) {
+            e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
+            de.complete.docDragData.droppedDocuments.map(doc => Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc));
+            return false;
+        }
+        return false;
+    };
+
+    _dropDisposer: any;
+    protected createDropTarget = (ele: HTMLDivElement) => {
+        this._dropDisposer?.();
+        if (ele) {
+            this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc);
+        }
+        // if (this.autoHeight) this.tryUpdateScrollHeight();
+    };
     @computed get theGraph() {
         return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
     }
@@ -80,6 +101,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
         TraceMobx();
         return (
             <div
+                ref={this.createDropTarget}
                 style={{
                     pointerEvents: !this.isContentActive() ? 'all' : undefined,
                     width: this.props.PanelWidth(),
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 98d611ca6..4895dcdc5 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -1,6 +1,7 @@
 import EquationEditor from 'equation-editor-react';
 import { IReactionDisposer } from 'mobx';
 import { observer } from 'mobx-react';
+import { TextSelection } from 'prosemirror-state';
 import * as ReactDOM from 'react-dom';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
@@ -21,7 +22,7 @@ export class EquationView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} setEditor={this.setEditor} tbox={tbox} />, this.dom);
+        ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />, this.dom);
         (this as any).dom = this.dom;
     }
     _editor: EquationEditor | undefined;
@@ -29,6 +30,9 @@ export class EquationView {
     destroy() {
         ReactDOM.unmountComponentAtNode(this.dom);
     }
+    setSelection() {
+        this._editor?.mathField.focus();
+    }
     selectNode() {
         this._editor?.mathField.focus();
     }
@@ -40,6 +44,7 @@ interface IEquationViewInternal {
     tbox: FormattedTextBox;
     width: number;
     height: number;
+    getPos: () => number;
     setEditor: (editor: EquationEditor | undefined) => void;
 }
 
@@ -67,11 +72,22 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
         return (
             <div
                 className="equationView"
+                onKeyDown={e => {
+                    if (e.key === 'Enter') {
+                        this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new TextSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos() + 1))));
+                        this.props.tbox.EditorView!.focus();
+                        e.preventDefault();
+                    }
+                    e.stopPropagation();
+                }}
+                onKeyPress={e => e.stopPropagation()}
                 style={{
                     position: 'relative',
                     display: 'inline-block',
                     width: this.props.width,
                     height: this.props.height,
+                    background: 'white',
+                    borderRadius: '10%',
                     bottom: 3,
                 }}>
                 <EquationEditor
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 31552cf1b..34bfb9e6f 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -2,7 +2,7 @@ import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinU
 import { redo, undo } from 'prosemirror-history';
 import { Schema } from 'prosemirror-model';
 import { splitListItem, wrapInList } from 'prosemirror-schema-list';
-import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
+import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state';
 import { liftTarget } from 'prosemirror-transform';
 import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc';
 import { GetEffectiveAcl } from '../../../../fields/util';
@@ -143,7 +143,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     bind('Alt-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any));
     bind('Shift-Ctrl-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any));
 
-    bind('Ctrl-m', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: 'math' + Utils.GenerateGuid() }))));
+    bind('Ctrl-m', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+        if (canEdit(state)) {
+            const tr = state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: 'math' + Utils.GenerateGuid() }));
+            dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1))));
+        }
+    });
 
     for (let i = 1; i <= 6; i++) {
         bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any));
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 2eb62c38d..7ddd1a2c4 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -295,6 +295,15 @@ export class RichTextRules {
                 return state.tr;
             }),
 
+            // create an inline equation node
+            // eq:<equation>>
+            new InputRule(new RegExp(/:eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+                const fieldKey = 'math' + Utils.GenerateGuid();
+                this.TextBox.dataDoc[fieldKey] = match[1];
+                const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
+                return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
+            }),
+
             // create an inline view of a document {{ <layoutKey> : <Doc> }}
             // {{:Doc}} => show default view of document
             // {{<layout>}} => show layout for this doc
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 5142b7da6..66d747bf7 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -157,6 +157,18 @@ export const nodes: { [index: string]: NodeSpec } = {
         },
     },
 
+    equation: {
+        inline: true,
+        attrs: {
+            fieldKey: { default: '' },
+        },
+        group: 'inline',
+        toDOM(node) {
+            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+            return ['div', { ...node.attrs, ...attrs }];
+        },
+    },
+
     // :: NodeSpec The text node.
     text: {
         group: 'inline',
@@ -260,20 +272,6 @@ export const nodes: { [index: string]: NodeSpec } = {
         },
     },
 
-    equation: {
-        inline: true,
-        attrs: {
-            fieldKey: { default: '' },
-        },
-        atom: true,
-        group: 'inline',
-        draggable: false,
-        toDOM(node) {
-            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
-            return ['div', { ...node.attrs, ...attrs }];
-        },
-    },
-
     video: {
         inline: true,
         attrs: {
-- 
cgit v1.2.3-70-g09d2


From b772fb95bb2f980f91da44d1442f52c4fb0b6eac Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 17 Oct 2022 19:07:32 -0400
Subject: added rotation around an arbitrary point to doc decos.  fixed show
 title for func plots and for shared docs in novice mode.

---
 src/Utils.ts                              |   4 +
 src/client/util/DragManager.ts            |   2 +-
 src/client/views/DocumentDecorations.scss |  56 +++++++------
 src/client/views/DocumentDecorations.tsx  | 135 ++++++++++++++++++++++--------
 src/client/views/StyleProvider.tsx        |   2 +-
 src/client/views/nodes/DocumentView.tsx   |  77 +++++++++--------
 6 files changed, 175 insertions(+), 101 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 6fc00040f..bf1f72774 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -243,6 +243,10 @@ export namespace Utils {
         return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]];
     }
 
+    export function rotPt(x: number, y: number, radAng: number) {
+        return { x: x * Math.cos(radAng) - y * Math.sin(radAng), y: x * Math.sin(radAng) + y * Math.cos(radAng) };
+    }
+
     function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
         if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 };
         const atob = { x: bx - ax, y: by - ay };
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 664933de0..160aba294 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -360,7 +360,7 @@ export namespace DragManager {
             let useDim = false;
             if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
                 ele = ele.parentElement.parentElement.parentElement;
-                const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1');
+                const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1');
                 if (rotStr) rot = Number(rotStr);
             } else {
                 useDim = true;
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 2e8d31478..e915fc88d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -8,12 +8,38 @@ $resizeHandler: 8px;
 .documentDecorations {
     position: absolute;
     z-index: 2000;
+
+    // Rotation handler
+    .documentDecorations-rotation {
+        border-radius: 100%;
+        height: 30;
+        width: 30;
+        right: -17;
+        top: calc(50% - 15px);
+        position: absolute;
+        pointer-events: all;
+        cursor: pointer;
+        background: white;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        text-align: center;
+        font-size: 30px;
+    }
+    .documentDecorations-rotationCenter {
+        position: absolute;
+        width: 6px;
+        height: 6px;
+        pointer-events: all;
+        background: green;
+        border-radius: 50%;
+    }
 }
 .documentDecorations-Dark {
     background: dimgray;
 }
+
 .documentDecorations-container {
-    z-index: $docDecorations-zindex;
     position: absolute;
     top: 0;
     left: 0;
@@ -29,7 +55,6 @@ $resizeHandler: 8px;
         flex-direction: row;
         gap: 2px;
 
-
         .documentDecorations-openButton {
             display: flex;
             align-items: center;
@@ -51,7 +76,7 @@ $resizeHandler: 8px;
                 color: #02600d;
             }
         }
-        
+
         .documentDecorations-closeButton {
             display: flex;
             align-items: center;
@@ -71,7 +96,7 @@ $resizeHandler: 8px;
             &:hover {
                 color: #a94442;
             }
-        
+
             > svg {
                 margin: 0;
             }
@@ -98,7 +123,7 @@ $resizeHandler: 8px;
             &:hover {
                 color: #a94442;
             }
-        
+
             > svg {
                 margin: 0;
             }
@@ -207,25 +232,6 @@ $resizeHandler: 8px;
         grid-column: 3;
     }
 
-    // Rotation handler
-    .documentDecorations-rotation {
-        border-radius: 100%;
-        height: 30;
-        width: 30;
-        right: -10;
-        top: 50%;
-        z-index: 1000000;
-        position: absolute;
-        pointer-events: all;
-        cursor: pointer;
-        background: white;
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        text-align: center;
-        font-size: 30px;
-    }
-
     // Border radius handler
     .documentDecorations-borderRadius {
         position: absolute;
@@ -353,8 +359,6 @@ $resizeHandler: 8px;
     }
 }
 
-
-
 .documentDecorations-background {
     background: lightblue;
     position: absolute;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8b8c32642..8f66eaca7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -12,7 +12,7 @@ import { InkField } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
-import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
@@ -30,7 +30,6 @@ import { LightboxView } from './LightboxView';
 import { DocumentView } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
-import { VideoBox } from './nodes/VideoBox';
 import React = require('react');
 
 @observer
@@ -311,49 +310,83 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             e => {} // clickEvent
         );
     };
+    @action
+    onRotateCenterDown = (e: React.PointerEvent): void => {
+        this._isRotating = true;
+        const seldocview = SelectionManager.Views()[0];
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]);
+                const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
+                const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI);
+                seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width);
+                seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height);
+
+                return false;
+            }), // moveEvent
+            action(() => {}), // upEvent
+            action((e, doubleTap) => {
+                if (doubleTap) {
+                    seldocview.rootDoc.rotateCenterX = 0.5;
+                    seldocview.rootDoc.rotateCenterY = 0.5;
+                }
+            })
+        );
+    };
 
     @action
     onRotateDown = (e: React.PointerEvent): void => {
         this._isRotating = true;
+        const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
         const rotateUndo = UndoManager.StartBatch('rotatedown');
         const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
         const centerPoint = { X: (this.Bounds.x + this.Bounds.r) / 2, Y: (this.Bounds.y + this.Bounds.b) / 2 };
+        const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
+        SelectionManager.Views().forEach(dv => {
+            const accumRot = (NumCast(dv.rootDoc._jitterRotation) / 180) * Math.PI;
+            const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y);
+            const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2];
+            const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot);
+            const unrotatedDocPos = { x: NumCast(dv.rootDoc.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.rootDoc.y) + localRotCtrOffset[1] - startRotCtr.y };
+            infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot });
+        });
+        const infoRot = (angle: number, isAbs = false) => {
+            SelectionManager.Views().forEach(
+                action(dv => {
+                    const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.rootDoc)!;
+                    const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle);
+                    infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle });
+                    dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x);
+                    dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y);
+                    dv.rootDoc._jitterRotation = ((isAbs ? 0 : NumCast(dv.rootDoc._jitterRotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
+                })
+            );
+        };
         setupMoveUpEvents(
             this,
             e,
             (e: PointerEvent, down: number[], delta: number[]) => {
                 const previousPoint = { X: e.clientX, Y: e.clientY };
                 const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
-                const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint);
+                const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen);
                 if (selectedInk.length) {
-                    angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint);
+                    deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, -deltaAng, centerPoint);
                 } else {
-                    SelectionManager.Views().forEach(dv => {
-                        const oldRotation = NumCast(dv.rootDoc._jitterRotation);
-                        // Rotation between -360 and 360
-                        let newRotation = (oldRotation - (angle * 180) / Math.PI) % 360;
-
-                        const diff = Math.round(newRotation / 45) - newRotation / 45;
-                        if (diff < 0.05) {
-                            console.log('show lines');
-                        }
-                        dv.rootDoc._jitterRotation = newRotation;
-                    });
+                    infoRot(deltaAng);
                 }
                 return false;
             }, // moveEvent
             action(() => {
-                SelectionManager.Views().forEach(dv => {
-                    const oldRotation = NumCast(dv.rootDoc._jitterRotation);
-                    const diff = Math.round(oldRotation / 45) - oldRotation / 45;
-                    if (diff < 0.05) {
-                        let newRotation = Math.round(oldRotation / 45) * 45;
-                        dv.rootDoc._jitterRotation = newRotation;
-                    }
-                });
+                const dv = SelectionManager.Views()[0];
+                const oldRotation = NumCast(dv.rootDoc._jitterRotation);
+                const diff = oldRotation - Math.round(oldRotation / 45) * 45;
+                if (Math.abs(diff) < 5) {
+                    infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
+                }
                 this._isRotating = false;
                 rotateUndo?.end();
-                UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
             }), // upEvent
             emptyFunction
         );
@@ -603,8 +636,26 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon');
     }
 
+    @observable _rotCenter = [0, 0];
+    @computed get rotCenter() {
+        if (SelectionManager.Views().length) {
+            const seldocview = SelectionManager.Views()[0];
+            const loccenter = Utils.rotPt(
+                NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width),
+                NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height),
+                (NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI
+            );
+            return seldocview.props
+                .ScreenToLocalTransform()
+                .inverse()
+                .transformPoint(loccenter.x + NumCast(seldocview.layoutDoc._width) / 2, loccenter.y + NumCast(seldocview.layoutDoc._height) / 2);
+        }
+        return this._rotCenter;
+    }
+
     render() {
-        const bounds = this.Bounds;
+        const { b, c, r, x, y } = this.Bounds;
+        const bounds = { b, c, r, x, y };
         const seldocview = SelectionManager.Views().slice(-1)[0];
         if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
             return null;
@@ -695,8 +746,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 <div
                     className="documentDecorations-background"
                     style={{
-                        transform: `rotate(${rotation}deg)`,
-                        transformOrigin: 'top  left',
                         width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                         height: bounds.b - bounds.y + this._resizeBorderWidth + 'px',
                         left: bounds.x - this._resizeBorderWidth / 2,
@@ -742,13 +791,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     {seldocview.props.renderDepth <= 1 || !seldocview.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')}
                                 </>
                             )}
-
-                            {useRotation && (
-                                <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                    <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
-                                </div>
-                            )}
-
                             {useRounding && (
                                 <div
                                     key="rad"
@@ -774,6 +816,31 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 </div>
                             )}
                         </div>
+
+                        {useRotation && (
+                            <>
+                                <div
+                                    style={{
+                                        position: 'absolute',
+                                        transform: `rotate(${rotation}deg)`,
+                                        width: this.Bounds.r - this.Bounds.x + 'px',
+                                        height: this.Bounds.b - this.Bounds.y + 'px',
+                                        left: this.Bounds.x,
+                                        top: this.Bounds.y,
+                                        pointerEvents: 'none',
+                                    }}>
+                                    <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                        <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
+                                    </div>
+                                </div>
+                                <div
+                                    className="documentDecorations-rotationCenter"
+                                    style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }}
+                                    onPointerDown={this.onRotateCenterDown}
+                                    onContextMenu={e => e.preventDefault()}
+                                />
+                            </>
+                        )}
                     </div>
                 )}
             </div>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8b256923a..aadcd7169 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -131,7 +131,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                     StrCast(
                         doc._showTitle,
                         props?.showTitle?.() ||
-                            (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any)
+                            (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any)
                                 ? doc.author === Doc.CurrentUserEmail
                                     ? StrCast(Doc.UserDoc().showTitle)
                                     : remoteDocHeader
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3da628cf1..1c48d47e9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1262,47 +1262,46 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
             Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
         );
-        const titleView =
-            !showTitle || Doc.noviceMode ? null : (
-                <div
-                    className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
-                    key="title"
-                    style={{
-                        position: this.headerMargin ? 'relative' : 'absolute',
-                        height: this.titleHeight,
-                        width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
-                        color: lightOrDark(background),
-                        background,
-                        pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
-                    }}>
-                    <EditableView
-                        ref={this._titleRef}
-                        contents={showTitle
-                            .split(';')
-                            .map(field => field.trim())
-                            .map(field => targetDoc[field]?.toString())
-                            .join('\\')}
-                        display={'block'}
-                        fontSize={10}
-                        GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
-                        SetValue={undoBatch((input: string) => {
-                            if (input?.startsWith('#')) {
-                                if (this.props.showTitle) {
-                                    this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
-                                } else {
-                                    Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
-                                }
+        const titleView = !showTitle ? null : (
+            <div
+                className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
+                key="title"
+                style={{
+                    position: this.headerMargin ? 'relative' : 'absolute',
+                    height: this.titleHeight,
+                    width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
+                    color: lightOrDark(background),
+                    background,
+                    pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+                }}>
+                <EditableView
+                    ref={this._titleRef}
+                    contents={showTitle
+                        .split(';')
+                        .map(field => field.trim())
+                        .map(field => targetDoc[field]?.toString())
+                        .join('\\')}
+                    display={'block'}
+                    fontSize={10}
+                    GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
+                    SetValue={undoBatch((input: string) => {
+                        if (input?.startsWith('#')) {
+                            if (this.props.showTitle) {
+                                this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
                             } else {
-                                var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
-                                if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
-                                if (showTitle.includes('Date') || showTitle === 'author') return true;
-                                Doc.SetInPlace(targetDoc, showTitle, value, true);
+                                Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
                             }
-                            return true;
-                        })}
-                    />
-                </div>
-            );
+                        } else {
+                            var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
+                            if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
+                            if (showTitle.includes('Date') || showTitle === 'author') return true;
+                            Doc.SetInPlace(targetDoc, showTitle, value, true);
+                        }
+                        return true;
+                    })}
+                />
+            </div>
+        );
         return this.props.hideTitle || (!showTitle && !showCaption) ? (
             this.contents
         ) : (
-- 
cgit v1.2.3-70-g09d2


From f42d592381c657b9aa57408ec7b1b35c035dd20c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 18 Oct 2022 11:06:38 -0400
Subject: hide rotate center by default

---
 src/client/views/DocumentDecorations.tsx | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8f66eaca7..91fb4e4df 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -388,7 +388,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 this._isRotating = false;
                 rotateUndo?.end();
             }), // upEvent
-            emptyFunction
+            action(e => (this._showRotCenter = !this._showRotCenter)) // clickEvent
         );
     };
 
@@ -636,6 +636,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon');
     }
 
+    @observable _showRotCenter = false;
     @observable _rotCenter = [0, 0];
     @computed get rotCenter() {
         if (SelectionManager.Views().length) {
@@ -833,12 +834,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                         <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
                                     </div>
                                 </div>
-                                <div
-                                    className="documentDecorations-rotationCenter"
-                                    style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }}
-                                    onPointerDown={this.onRotateCenterDown}
-                                    onContextMenu={e => e.preventDefault()}
-                                />
+                                {!this._showRotCenter ? null : (
+                                    <div
+                                        className="documentDecorations-rotationCenter"
+                                        style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }}
+                                        onPointerDown={this.onRotateCenterDown}
+                                        onContextMenu={e => e.preventDefault()}
+                                    />
+                                )}
                             </>
                         )}
                     </div>
-- 
cgit v1.2.3-70-g09d2


From 04b4e1fdf075c35adefc922abd0c75895a23cf12 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 18 Oct 2022 11:37:27 -0400
Subject: fixed up off-center rotation for ink

---
 src/client/documents/Documents.ts        |  1 +
 src/client/views/DocumentDecorations.tsx | 44 +++++++++++++++++++++-----------
 2 files changed, 30 insertions(+), 15 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2500eafab..2d82ffdf9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -984,6 +984,7 @@ export namespace Docs {
             I.strokeEndMarker = arrowEnd;
             I.strokeDash = dash;
             I.tool = tool;
+            I.fitWidth = true;
             I['text-align'] = 'center';
             I.title = 'ink';
             I.x = options.x as number;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 91fb4e4df..d79cf014d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -310,6 +310,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             e => {} // clickEvent
         );
     };
+
+    setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
+        const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]);
+        const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
+        const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI);
+        seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width);
+        seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height);
+    };
+
     @action
     onRotateCenterDown = (e: React.PointerEvent): void => {
         this._isRotating = true;
@@ -318,15 +327,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             this,
             e,
             action((e: PointerEvent, down: number[], delta: number[]) => {
-                const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]);
-                const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
-                const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI);
-                seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width);
-                seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height);
-
+                this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]);
                 return false;
             }), // moveEvent
-            action(() => {}), // upEvent
+            action(action(() => (this._isRotating = false))), // upEvent
             action((e, doubleTap) => {
                 if (doubleTap) {
                     seldocview.rootDoc.rotateCenterX = 0.5;
@@ -342,8 +346,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
         const rotateUndo = UndoManager.StartBatch('rotatedown');
         const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
-        const centerPoint = { X: (this.Bounds.x + this.Bounds.r) / 2, Y: (this.Bounds.y + this.Bounds.b) / 2 };
+        const centerPoint = this.rotCenter.slice();
         const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
+        const seldocview = SelectionManager.Views()[0];
         SelectionManager.Views().forEach(dv => {
             const accumRot = (NumCast(dv.rootDoc._jitterRotation) / 180) * Math.PI;
             const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y);
@@ -372,18 +377,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
                 const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen);
                 if (selectedInk.length) {
-                    deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, -deltaAng, centerPoint);
+                    deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, deltaAng, rcScreen);
+                    this.setRotateCenter(seldocview, centerPoint);
                 } else {
                     infoRot(deltaAng);
                 }
                 return false;
             }, // moveEvent
             action(() => {
-                const dv = SelectionManager.Views()[0];
-                const oldRotation = NumCast(dv.rootDoc._jitterRotation);
+                const oldRotation = NumCast(seldocview.rootDoc._jitterRotation);
                 const diff = oldRotation - Math.round(oldRotation / 45) * 45;
                 if (Math.abs(diff) < 5) {
-                    infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
+                    if (selectedInk.length) {
+                        InkStrokeProperties.Instance.rotateInk(selectedInk, ((Math.round(oldRotation / 45) * 45 - oldRotation) / 180) * Math.PI, rcScreen);
+                    } else {
+                        infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
+                    }
+                }
+                if (selectedInk.length) {
+                    this.setRotateCenter(seldocview, centerPoint);
                 }
                 this._isRotating = false;
                 rotateUndo?.end();
@@ -830,9 +842,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                         top: this.Bounds.y,
                                         pointerEvents: 'none',
                                     }}>
-                                    <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                        <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
-                                    </div>
+                                    {this._isRotating ? null : (
+                                        <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                            <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
+                                        </div>
+                                    )}
                                 </div>
                                 {!this._showRotCenter ? null : (
                                     <div
-- 
cgit v1.2.3-70-g09d2


From 880023cb520f82fa1bfd4740450a18e86e817b0f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 18 Oct 2022 13:00:55 -0400
Subject: starting to fix resizing rotated docs.

---
 src/client/views/DocumentDecorations.scss | 2 +-
 src/client/views/DocumentDecorations.tsx  | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index e915fc88d..5c8dd36cc 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -14,7 +14,7 @@ $resizeHandler: 8px;
         border-radius: 100%;
         height: 30;
         width: 30;
-        right: -17;
+        right: -30;
         top: calc(50% - 15px);
         position: absolute;
         pointer-events: all;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d79cf014d..9881ef9f1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -579,10 +579,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             } else doc._height = actualdH;
                         }
                     } else {
+                        const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
+                        const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._jitterRotation) / 180) * Math.PI);
+
                         const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
                         dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
                         dW && (doc._width = actualdW);
                         dH && (doc._autoHeight = false);
+
+                        const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
+                        const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._jitterRotation) / 180) * Math.PI);
+                        doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc
+                        doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]);
                     }
                     doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
                     doc.y = (doc.y || 0) + (dragBottom ? 0 : dY * (actualdH - docheight));
-- 
cgit v1.2.3-70-g09d2


From a81ab2e6f75681bc4fb3a7b49d2056144e396b94 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 19 Oct 2022 12:38:19 -0400
Subject: fixed erasing straight line stroke segments caused by intersections
 with other strokes. cleaned up more with gestures.  changed docView lock icon
 to be part of docDecorations.

---
 src/client/views/DocumentDecorations.scss          | 28 ++++++-
 src/client/views/DocumentDecorations.tsx           | 41 +++++++++-
 src/client/views/GestureOverlay.tsx                | 21 +++---
 src/client/views/StyleProvider.scss                | 18 ++---
 src/client/views/StyleProvider.tsx                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 58 ++++++--------
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/ImageBox.tsx                |  5 +-
 src/client/views/nodes/button/FontIconBox.tsx      | 20 ++---
 src/pen-gestures/GestureUtils.ts                   |  2 -
 src/pen-gestures/ndollar.ts                        | 88 +++++++++-------------
 11 files changed, 148 insertions(+), 137 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 5c8dd36cc..4e0b061a6 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,7 +1,7 @@
 @import 'global/globalCssVariables';
 
 $linkGap: 3px;
-$headerHeight: 20px;
+$headerHeight: 25px;
 $resizeHandler: 8px;
 
 .documentDecorations-Dark,
@@ -54,6 +54,8 @@ $resizeHandler: 8px;
         grid-column-end: 4;
         flex-direction: row;
         gap: 2px;
+        pointer-events: all;
+        cursor: move;
 
         .documentDecorations-openButton {
             display: flex;
@@ -236,8 +238,8 @@ $resizeHandler: 8px;
     .documentDecorations-borderRadius {
         position: absolute;
         border-radius: 100%;
-        left: 3px;
-        top: 23px;
+        left: 7px;
+        top: 27px;
         background: $medium-gray;
         height: 10;
         width: 10;
@@ -245,6 +247,21 @@ $resizeHandler: 8px;
         cursor: nwse-resize;
     }
 
+    .documentDecorations-lock {
+        position: absolute;
+        background: black;
+        right: 11;
+        top: 30px;
+        color: gray;
+        height: 14;
+        width: 14;
+        pointer-events: all;
+        margin: auto;
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+    }
+
     .documentDecorations-rotationPath {
         position: absolute;
         width: 100%;
@@ -260,6 +277,7 @@ $resizeHandler: 8px;
         cursor: nwse-resize;
         background: unset;
         opacity: 1;
+        transform: scale(2);
     }
 
     .documentDecorations-topLeftResizer {
@@ -314,6 +332,7 @@ $resizeHandler: 8px;
     .documentDecorations-topLeftResizer:hover,
     .documentDecorations-bottomRightResizer:hover {
         opacity: 1;
+        background: black;
     }
 
     .documentDecorations-bottomRightResizer {
@@ -325,6 +344,7 @@ $resizeHandler: 8px;
         cursor: nesw-resize;
         background: unset;
         opacity: 1;
+        transform: scale(2);
     }
 
     .documentDecorations-topRightResizer {
@@ -339,7 +359,6 @@ $resizeHandler: 8px;
 
     .documentDecorations-topRightResizer:hover,
     .documentDecorations-bottomLeftResizer:hover {
-        cursor: nesw-resize;
         background: black;
         opacity: 1;
     }
@@ -392,6 +411,7 @@ $resizeHandler: 8px;
     justify-content: center;
     align-items: center;
     gap: 5px;
+    top: 4px;
     background: $light-gray;
 }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9881ef9f1..6cf7df357 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -140,6 +140,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         }
     };
 
+    @action onContainerDown = (e: React.PointerEvent): void => {
+        setupMoveUpEvents(
+            this,
+            e,
+            e => this.onBackgroundMove(true, e),
+            e => {},
+            emptyFunction
+        );
+    };
+
     @action onTitleDown = (e: React.PointerEvent): void => {
         setupMoveUpEvents(
             this,
@@ -311,6 +321,27 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         );
     };
 
+    @action
+    onLockDown = (e: React.PointerEvent): void => {
+        // Call util move event function
+        setupMoveUpEvents(
+            this, // target
+            e, // pointerEvent
+            returnFalse, // moveEvent
+            emptyFunction, // upEvent
+            e => {
+                UndoManager.RunInBatch(
+                    () =>
+                        SelectionManager.Views().map(dv => {
+                            dv.rootDoc._lockedPosition = !dv.rootDoc._lockedPosition;
+                            dv.rootDoc._pointerEvents = dv.rootDoc._lockedPosition ? 'none' : undefined;
+                        }),
+                    'toggleBackground'
+                );
+            } // clickEvent
+        );
+    };
+
     setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
         const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]);
         const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
@@ -791,7 +822,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                 width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
                                 height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
                             }}>
-                            <div className="documentDecorations-topbar">
+                            <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}>
                                 {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                 {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
                                 {hideResizers ? <div /> : titleArea}
@@ -817,21 +848,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     key="rad"
                                     style={{
                                         background: `${this._isRounding ? Colors.MEDIUM_BLUE : undefined}`,
-                                        left: `${radiusHandleLocation + 3}`,
-                                        top: `${radiusHandleLocation + 23}`,
+                                        transform: `translate(${radiusHandleLocation}px, ${radiusHandleLocation}px)`,
                                     }}
                                     className={`documentDecorations-borderRadius`}
                                     onPointerDown={this.onRadiusDown}
                                     onContextMenu={e => e.preventDefault()}
                                 />
                             )}
+                            <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
+                                <FontAwesomeIcon size="sm" icon="lock" />
+                            </div>
 
                             {hideDocumentButtonBar ? null : (
                                 <div
                                     className="link-button-container"
                                     key="links"
                                     style={{
-                                        transform: ` translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
+                                        transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
                                     }}>
                                     <DocumentButtonBar views={SelectionManager.Views} />
                                 </div>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 23b03bc50..362aa3c86 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,7 +1,7 @@
 import React = require('react');
 import * as fitCurve from 'fit-curve';
 import { action, computed, observable, runInAction, trace } from 'mobx';
-import { Doc } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { InkData, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
@@ -49,7 +49,7 @@ interface GestureOverlayProps {
 export class GestureOverlay extends Touchable<GestureOverlayProps> {
     static Instance: GestureOverlay;
 
-    @observable public InkShape: string = '';
+    @observable public InkShape: Opt<GestureUtils.Gestures>;
     @observable public SavedColor?: string;
     @observable public SavedWidth?: number;
     @observable public Tool: ToolglassTools = ToolglassTools.None;
@@ -644,8 +644,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
-            //push first points to so interactionUtil knows pointer is up
-            this._points.push({ X: this._points[0].X, Y: this._points[0].Y });
 
             const initialPoint = this._points[0];
             const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
@@ -685,10 +683,10 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
             //if any of the shape is activated in the CollectionFreeFormViewChrome
             else if (this.InkShape) {
                 this.makeBezierPolygon(this.InkShape, false);
-                this.dispatchGesture(GestureUtils.Gestures.Stroke);
+                this.dispatchGesture(this.InkShape);
                 this._points.length = 0;
                 if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
-                    this.InkShape = '';
+                    this.InkShape = undefined;
                     Doc.ActiveTool = InkTool.None;
                 }
             }
@@ -703,12 +701,13 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                         case GestureUtils.Gestures.Rectangle:
                         case GestureUtils.Gestures.Circle:
                             this.makeBezierPolygon(result.Name, true);
-                        case GestureUtils.Gestures.StartBracket:
-                        case GestureUtils.Gestures.EndBracket:
                             actionPerformed = this.dispatchGesture(result.Name);
                             break;
                         case GestureUtils.Gestures.Line:
-                            actionPerformed = this.handleLineGesture();
+                            if (!(actionPerformed = this.handleLineGesture())) {
+                                this.makeBezierPolygon(result.Name, true);
+                                actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Stroke);
+                            }
                             break;
                         case GestureUtils.Gestures.Scribble:
                             console.log('scribble');
@@ -972,7 +971,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                                 ActiveDash(),
                                 1,
                                 1,
-                                this.InkShape,
+                                this.InkShape ?? '',
                                 'none',
                                 1.0,
                                 false
@@ -999,7 +998,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                             ActiveDash(),
                             1,
                             1,
-                            this.InkShape,
+                            this.InkShape ?? '',
                             'none',
                             1.0,
                             false
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 8929954c8..b1c97164a 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -1,11 +1,11 @@
 .styleProvider-lock {
-    font-size: 12px;
-    width: 20; 
-    height: 20; 
-    position: absolute; 
-    right: -25; 
-    top: -5;
-    background: transparent;
+    font-size: 10;
+    width: 15;
+    height: 15;
+    position: absolute;
+    right: -0;
+    top: 0;
+    background: black;
     pointer-events: all;
     opacity: 0.3;
     display: flex;
@@ -15,7 +15,7 @@
     cursor: default;
 }
 .styleProvider-lock:hover {
-    opacity:1;
+    opacity: 1;
 }
 
 .styleProvider-treeView-icon,
@@ -26,4 +26,4 @@
 
 .styleProvider-treeView-icon {
     opacity: 0;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index aadcd7169..a5a886f42 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -276,7 +276,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.Decorations:
             if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
                 return doc &&
-                    (isBackground() || selected) &&
+                    isBackground() &&
                     !Doc.IsSystem(doc) &&
                     (props?.renderDepth || 0) > 0 &&
                     ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index daf69d4f6..983cf4f9b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -568,6 +568,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
+            case GestureUtils.Gestures.Line:
+                break;
             default:
             case GestureUtils.Gestures.Circle:
             case GestureUtils.Gestures.Rectangle:
@@ -602,33 +604,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 e.stopPropagation();
                 break;
             case GestureUtils.Gestures.Rectangle:
-                const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
-                const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
-                const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] };
-                const bWidth = bounds.r - bounds.x;
-                const bHeight = bounds.b - bounds.y;
-                const sel = this.getActiveDocuments().filter(doc => {
-                    const l = NumCast(doc.x);
-                    const r = l + doc[WidthSym]();
-                    const t = NumCast(doc.y);
-                    const b = t + doc[HeightSym]();
-                    const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t);
-                    if (pass) {
-                        doc.x = l - bounds.x - bWidth / 2;
-                        doc.y = t - bounds.y - bHeight / 2;
-                    }
-                    return pass;
-                });
-                this.addDocument(Docs.Create.FreeformDocument(sel, { title: 'nested collection', x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 }));
-                sel.forEach(d => this.props.removeDocument?.(d));
-                e.stopPropagation();
-                break;
-            case GestureUtils.Gestures.StartBracket:
-                const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
-                this._inkToTextStartX = start[0];
-                this._inkToTextStartY = start[1];
-                break;
-            case GestureUtils.Gestures.EndBracket:
                 if (this._inkToTextStartX && this._inkToTextStartY) {
                     const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
                     const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === 'rtf' && s.color);
@@ -805,15 +780,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
                 const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
                 for (var i = 0; i < inkData.length - 3; i += 4) {
-                    const intersects = Array.from(
-                        new Set(
-                            InkField.Segment(inkData, i).intersects({
-                                // compute all unique intersections
-                                p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
-                                p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
-                            }) as (number | string)[]
-                        )
-                    ); // convert to more manageable union array type
+                    const rawIntersects = InkField.Segment(inkData, i).intersects({
+                        // compute all unique intersections
+                        p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+                        p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
+                    });
+                    const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type
+                    if (intersects.length) {
+                        console.log();
+                    }
                     // return tuples of the inkingStroke intersected, and the t value of the intersection
                     intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
                 }
@@ -862,6 +837,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return segments;
     };
 
+    // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
+    // call in a test for linearity
+    bintersects = (curve: Bezier, otherCurve: Bezier) => {
+        if ((otherCurve as any)._linear) {
+            return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
+        }
+        return curve.intersects(otherCurve);
+    };
+
     /**
      * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
      * ink strokes in the current collection.
@@ -886,7 +870,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
 
                     const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
-                    curve.intersects(otherCurve).forEach((val: string | number, i: number) => {
+                    this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => {
                         // Converting the Bezier.js Split type to a t-value number.
                         const t = +val.toString().split('/')[0];
                         if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack!  don't know why but intersection points are doubled from bezier.js (but not identical).
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1c48d47e9..040e03150 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1082,7 +1082,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     <DocumentLinksButton
                         View={this.props.DocumentView()}
                         scaling={this.linkButtonInverseScaling}
-                        Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -28]}
+                        Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -32]}
                     />
                 )}
                 {audioView}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 959c641a8..461d6984d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { createSchema } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, NumCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -77,10 +77,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
             { fireImmediately: true, delay: 1000 }
         );
+        const layoutDoc = this.layoutDoc;
         this._disposers.path = reaction(
             () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
             ({ nativeSize, width }) => {
-                if (true || !this.layoutDoc._height) {
+                if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 6eaf3c31a..fd0c0d141 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -771,36 +771,26 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
     CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
 }
 
-/** INK
- *      setActiveTool
- *      setStrokeWidth
- *      setStrokeColor
- **/
-
-ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) {
+ScriptingGlobals.add(function setActiveTool(tool: InkTool | GestureUtils.Gestures, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
     if (checkResult) {
         return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent';
     }
-    if ([GestureUtils.Gestures.Circle, GestureUtils.Gestures.Rectangle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Triangle].includes(tool as any)) {
+    if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
         if (GestureOverlay.Instance.InkShape === tool) {
             Doc.ActiveTool = InkTool.None;
-            GestureOverlay.Instance.InkShape = InkTool.None;
+            GestureOverlay.Instance.InkShape = undefined;
         } else {
             Doc.ActiveTool = InkTool.Pen;
-            GestureOverlay.Instance.InkShape = tool;
+            GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
         }
     } else if (tool) {
         // pen or eraser
         if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
             Doc.ActiveTool = InkTool.None;
-        } else if (tool == InkTool.Write) {
-            // console.log("write mode selected - create groupDoc here!", tool)
-            Doc.ActiveTool = tool;
-            GestureOverlay.Instance.InkShape = '';
         } else {
             Doc.ActiveTool = tool as any;
-            GestureOverlay.Instance.InkShape = '';
+            GestureOverlay.Instance.InkShape = undefined;
         }
     } else {
         Doc.ActiveTool = InkTool.None;
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 2d3b1fdb8..41917aac9 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -19,8 +19,6 @@ export namespace GestureUtils {
 
     export enum Gestures {
         Line = 'line',
-        StartBracket = 'startbracket',
-        EndBracket = 'endbracket',
         Stroke = 'stroke',
         Scribble = 'scribble',
         Text = 'text',
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index b10a9da17..3ee9506cb 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -145,7 +145,7 @@ export class Result {
 //
 // NDollarRecognizer constants
 //
-const NumMultistrokes = 6;
+let NumMultistrokes = 0;
 const NumPoints = 96;
 const SquareSize = 250.0;
 const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35)
@@ -162,73 +162,59 @@ const AngleSimilarityThreshold = Deg2Rad(30.0);
 // NDollarRecognizer class
 //
 export class NDollarRecognizer {
-    public Multistrokes: Multistroke[];
+    public Multistrokes: Multistroke[] = [];
 
-    /**
-     * @IMPORTANT - IF YOU'RE ADDING A NEW GESTURE, BE SURE TO INCREMENT THE NumMultiStrokes CONST RIGHT ABOVE THIS CLASS.
-     */
     constructor(
         useBoundedRotationInvariance: boolean // constructor
     ) {
         //
         // one predefined multistroke for each multistroke type
         //
-        this.Multistrokes = new Array(NumMultistrokes);
-        this.Multistrokes[0] = new Multistroke(
-            GestureUtils.Gestures.Rectangle,
-            useBoundedRotationInvariance,
-            new Array(
+        this.Multistrokes.push(
+            new Multistroke(
+                GestureUtils.Gestures.Rectangle,
+                useBoundedRotationInvariance,
                 new Array(
-                    new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
-                    new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
-                    new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
-                    new Point(106, 146), //new Point(80, 150), new Point(50, 146),
-                    new Point(30, 143)
+                    new Array(
+                        new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
+                        new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
+                        new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
+                        new Point(106, 146), //new Point(80, 150), new Point(50, 146),
+                        new Point(30, 143)
+                    )
                 )
             )
         );
-        this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347))));
-        this.Multistrokes[2] = new Multistroke(
-            GestureUtils.Gestures.StartBracket,
-            useBoundedRotationInvariance,
-            new Array(
-                // new Array(new Point(145, 20), new Point(30, 21), new Point(34, 150))
-                new Array(new Point(31, 25), new Point(145, 20), new Point(31, 25), new Point(34, 150))
+        this.Multistrokes.push(new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347)))));
+        this.Multistrokes.push(
+            new Multistroke(
+                GestureUtils.Gestures.Triangle, // equilateral
+                useBoundedRotationInvariance,
+                new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
             )
         );
-        this.Multistrokes[3] = new Multistroke(
-            GestureUtils.Gestures.EndBracket,
-            useBoundedRotationInvariance,
-            new Array(
-                // new Array(new Point(150, 21), new Point(149, 150), new Point(26, 152))
-                // new Array(new Point(150, 150), new Point(150, 0), new Point(150, 150), new Point(0, 150))
-                new Array(new Point(10, 100), new Point(100, 100), new Point(150, 12), new Point(200, 103), new Point(300, 100))
-            )
-        );
-        this.Multistrokes[4] = new Multistroke(
-            GestureUtils.Gestures.Triangle, // equilateral
-            useBoundedRotationInvariance,
-            new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100)))
-        );
-        this.Multistrokes[5] = new Multistroke(
-            GestureUtils.Gestures.Circle,
-            useBoundedRotationInvariance,
-            new Array(
+        this.Multistrokes.push(
+            new Multistroke(
+                GestureUtils.Gestures.Circle,
+                useBoundedRotationInvariance,
                 new Array(
-                    new Point(200, 250),
-                    new Point(240, 230),
-                    new Point(248, 210),
-                    new Point(248, 190),
-                    new Point(240, 170),
-                    new Point(200, 150),
-                    new Point(160, 170),
-                    new Point(151, 190),
-                    new Point(151, 210),
-                    new Point(160, 230),
-                    new Point(201, 250)
+                    new Array(
+                        new Point(200, 250),
+                        new Point(240, 230),
+                        new Point(248, 210),
+                        new Point(248, 190),
+                        new Point(240, 170),
+                        new Point(200, 150),
+                        new Point(160, 170),
+                        new Point(151, 190),
+                        new Point(151, 210),
+                        new Point(160, 230),
+                        new Point(201, 250)
+                    )
                 )
             )
         );
+        NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
         //
         // PREDEFINED STROKES
         //
-- 
cgit v1.2.3-70-g09d2


From ba73479fe6c0594ebe37eed6cf295e5adc49c8e1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 25 Oct 2022 11:10:12 -0400
Subject: various tweaks: show border highlights in tree views, uniform
 treatment of embedding using title bar, even for free form views, choose
 bestAlias when dragging from files menu,  text boxes honor document moves,
 show title for timeline annotations, fix dragoffset for stacks with
 buttons/description headers,

---
 src/Utils.ts                                       |   6 +-
 src/client/util/DocumentManager.ts                 |   4 +-
 src/client/util/DragManager.ts                     |  18 ++-
 src/client/views/DocumentDecorations.tsx           |   7 +-
 src/client/views/StyleProvider.tsx                 |  17 ++-
 src/client/views/collections/CollectionSubView.tsx |   7 +-
 .../views/collections/CollectionTreeView.tsx       |   7 +-
 src/client/views/collections/TreeView.tsx          |  19 +--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  32 +++--
 src/client/views/nodes/DocumentView.tsx            | 136 ++++++++++-----------
 .../views/nodes/formattedText/FormattedTextBox.tsx |   3 +
 src/fields/Doc.ts                                  |   5 +
 12 files changed, 133 insertions(+), 128 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index bf1f72774..5e0514bc6 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -728,10 +728,10 @@ export function getWordAtPoint(elem: any, x: number, y: number): string | undefi
     return undefined;
 }
 
-export function hasDescendantTarget(x: number, y: number, target: HTMLDivElement | null) {
+export function isTargetChildOf(ele: HTMLDivElement | null, target: Element | null) {
     let entered = false;
-    for (let child = document.elementFromPoint(x, y); !entered && child; child = child.parentElement) {
-        entered = entered || child === target;
+    for (let child = target; !entered && child; child = child.parentElement) {
+        entered = child === ele;
     }
     return entered;
 }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 9a46d20de..b046d950f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -335,9 +335,7 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) {
     } else {
         const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
         const showDoc = context || doc;
-        const bestAlias = showDoc === Doc.GetProto(showDoc) ? DocListCast(showDoc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : showDoc;
-
-        CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
+        CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
     }
 }
 ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 160aba294..899764866 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -206,7 +206,7 @@ export namespace DragManager {
                         !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
                             ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
                             : docDragData.dropAction === 'alias'
-                            ? Doc.MakeAlias(d)
+                            ? Doc.BestAlias(d)
                             : docDragData.dropAction === 'proto'
                             ? Doc.GetProto(d)
                             : docDragData.dropAction === 'copy'
@@ -260,10 +260,6 @@ export namespace DragManager {
         StartDrag([ele], dragData, downX, downY, options);
     }
 
-    export function StartImgDrag(ele: HTMLElement, downX: number, downY: number) {
-        StartDrag([ele], {}, downX, downY);
-    }
-
     export function SetSnapLines(horizLines: number[], vertLines: number[]) {
         SnappingManager.setSnapLines(horizLines, vertLines);
     }
@@ -325,7 +321,7 @@ export namespace DragManager {
     export let DocDragData: DocumentDragData | undefined;
     export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
         if (dragData.dropAction === 'none') return;
-        DocDragData = dragData instanceof DocumentDragData ? dragData : undefined;
+        DocDragData = dragData as DocumentDragData;
         const batch = UndoManager.StartBatch('dragging');
         eles = eles.filter(e => e);
         CanEmbed = dragData.canEmbed || false;
@@ -357,15 +353,15 @@ export namespace DragManager {
         let rot = 0;
         const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
         const dragElements = eles.map(ele => {
-            let useDim = false;
+            // bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element
+            let useDim = false; // if doc is rotated by freeformview, then the dragged elements width and height won't reflect the unrotated dimensions, so we need to rely on the element knowing its own width/height.  \
+            // if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu)
             if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
                 ele = ele.parentElement.parentElement.parentElement;
-                const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1');
-                if (rotStr) rot = Number(rotStr);
+                rot = Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0);
             } else {
                 useDim = true;
             }
-            if (rot < 0) rot += 360;
             if (!ele.parentNode) dragDiv.appendChild(ele);
             const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
             const children = Array.from(dragElement.children);
@@ -387,7 +383,7 @@ export namespace DragManager {
             const rect = ele.getBoundingClientRect();
             const w = ele.offsetWidth || rect.width;
             const h = ele.offsetHeight || rect.height;
-            const rotR = -(rot / 180) * Math.PI;
+            const rotR = -((rot < 0 ? rot + 360 : rot) / 180) * Math.PI;
             const tl = [0, 0];
             const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
             const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6cf7df357..10517d829 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -177,6 +177,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         );
         dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top);
         dragData.moveDocument = dragDocView.props.moveDocument;
+        dragData.removeDocument = dragDocView.props.removeDocument;
         dragData.isDocDecorationMove = true;
         dragData.canEmbed = dragTitle;
         this._hidden = this.Interacting = true;
@@ -250,8 +251,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         if (selectedDocs.length) {
             if (e.ctrlKey) {
                 // open an alias in a new tab with Ctrl Key
-                const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
-                CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), 'right');
+                CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), 'right');
             } else if (e.shiftKey) {
                 // open centered in a new workspace with Shift Key
                 const alias = Doc.MakeAlias(selectedDocs[0].props.Document);
@@ -766,6 +766,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 onBlur={e => !hideTitle && this.titleBlur()}
                 onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
                 onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
+                onPointerDown={e => e.stopPropagation()}
             />
         ) : (
             <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}>
@@ -825,7 +826,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}>
                                 {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                 {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
-                                {hideResizers ? <div /> : titleArea}
+                                {titleArea}
                                 {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
                             </div>
                             {hideResizers ? null : (
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index a5a886f42..2eb3dd532 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -3,8 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, runInAction } from 'mobx';
 import { extname } from 'path';
 import { Doc, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types';
-import { DashColor, lightOrDark } from '../../Utils';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen } from '../util/DocumentManager';
 import { ColorScheme } from '../util/SettingsManager';
@@ -42,6 +42,7 @@ export enum StyleProp {
     FontSize = 'fontSize', // size of text font
     FontFamily = 'fontFamily', // font family of text
     FontWeight = 'fontWeight', // font weight of text
+    Highlighting = 'highlighting', // border highlighting
 }
 
 function darkScheme() {
@@ -107,6 +108,18 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             if (doc?._viewType === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' };
             allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
             return allSorts;
+        case StyleProp.Highlighting:
+            if (doc) {
+                const highlightIndex = Doc.isBrushedHighlightedDegree(doc);
+                const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
+                const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
+                const excludeTypes = !props?.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
+                let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents.  need to flag a document as not highlightable in a more direct way
+                if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') {
+                    return { highlightStyle, highlightColor, highlightIndex };
+                }
+            }
+            return undefined;
         case StyleProp.DocContents:
             return undefined;
         case StyleProp.WidgetColor:
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 30759b766..7bc273d7d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -218,12 +218,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                     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 ||
-                            this.props.Document.allowOverlayDrop ||
-                            Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
+                        const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || 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 {
                         ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index fe5dc17f5..1169f692a 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -147,6 +147,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         }
     };
 
+    screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this._headerHeight);
+
     @action
     remove = (doc: Doc | Doc[]): boolean => {
         const docs = doc instanceof Doc ? [doc] : doc;
@@ -275,7 +277,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             dropAction,
             this.props.addDocTab,
             this.props.styleProvider,
-            this.props.ScreenToLocalTransform,
+            this.screenToLocalTransform,
             this.isContentActive,
             this.panelWidth,
             this.props.renderDepth,
@@ -377,6 +379,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     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;
 
+    @observable _headerHeight = 0;
     contentFunc = () => {
         const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
         const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
@@ -384,7 +387,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return [
             <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
                 {!this.buttonMenu && !this.noviceExplainer ? null : (
-                    <div className="documentButtonMenu">
+                    <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}>
                         {this.buttonMenu}
                         {this.noviceExplainer}
                     </div>
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 409b615f4..b9f86fa6b 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -10,11 +10,12 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
@@ -31,7 +32,6 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
 import { CollectionView } from './CollectionView';
 import './TreeView.scss';
 import React = require('react');
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 
 export interface TreeViewProps {
     treeView: CollectionTreeView;
@@ -792,12 +792,14 @@ export class TreeView extends React.Component<TreeViewProps> {
     titleStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (!doc || doc !== this.doc) return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
 
+        // prettier-ignore
         switch (property.split(':')[0]) {
-            case StyleProp.Opacity:
-                return this.props.treeView.outlineMode ? undefined : 1;
-            case StyleProp.BackgroundColor:
-                return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
+            case StyleProp.Opacity:         return this.props.treeView.outlineMode ? undefined : 1;
+            case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
+            case StyleProp.Highlighting:    if (this.props.treeView.outlineMode) return undefined;
             case StyleProp.DocContents:
+                const highlightIndex = this.props.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.isBrushedHighlightedDegree(doc);
+                const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                 return this.props.treeView.outlineMode ? null : (
                     <div
                         className="treeView-label"
@@ -805,13 +807,13 @@ export class TreeView extends React.Component<TreeViewProps> {
                             // just render a title for a tree view label (identified by treeViewDoc being set in 'props')
                             maxWidth: props?.PanelWidth() || undefined,
                             background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor),
+                            outline: `solid ${highlightColor} ${highlightIndex}px`,
                         }}>
                         {StrCast(doc?.title)}
                     </div>
                 );
-            default:
-                return this.props?.treeView?.props.styleProvider?.(doc, props, property);
         }
+        return this.props?.treeView?.props.styleProvider?.(doc, props, property);
     };
     embeddedStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (property.startsWith(StyleProp.Decorations)) return null;
@@ -1011,6 +1013,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 pinToPres={this.props.treeView.props.pinToPres}
                 disableDocBrushing={this.props.treeView.props.disableDocBrushing}
                 bringToFront={returnFalse}
+                scriptContext={this}
             />
         );
     };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 88422cbd4..eca95e1e0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -113,7 +113,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _clusterSets: Doc[][] = [];
     @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
-    @observable _marqueeRef = React.createRef<HTMLDivElement>();
+    @observable _marqueeRef: HTMLDivElement | null = null;
     @observable _marqueeViewRef = React.createRef<MarqueeView>();
     @observable ChildDrag: DocumentView | undefined; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
     @observable _brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; // highlighted region of freeform canvas used by presentations to indicate a region
@@ -1011,22 +1011,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         switch (Doc.UserDoc().freeformScrollMode) {
             case freeformScrollMode.Pan:
                 // if shift is selected then zoom
-                if(e.ctrlKey) {
+                if (e.ctrlKey) {
                     if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
                         // things that can scroll vertically should do that instead of zooming
                     } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
                         !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?
                     }
-                // otherwise pan
+                    // otherwise pan
                 } else if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
                     // things that can scroll vertically should do that instead of zooming
                 } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
                     const dx = e.deltaX;
                     const dy = e.deltaY;
                     if (e.shiftKey) {
-                        !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 }); 
+                        !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 });
                     } else {
-                        !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dx, deltaY: dy }); 
+                        !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dx, deltaY: dy });
                     }
                 }
                 break;
@@ -1548,9 +1548,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         setTimeout(
             action(() => {
                 this._firstRender = false;
-
-                this._marqueeRef.current?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
-
                 this._disposers.groupBounds = reaction(
                     () => {
                         if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) {
@@ -1668,7 +1665,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     componentWillUnmount() {
         Object.values(this._disposers).forEach(disposer => disposer?.());
-        this._marqueeRef.current?.removeEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
+        this._marqueeRef?.removeEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
     }
 
     @action
@@ -1681,10 +1678,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if ((e as any).handlePan || this.props.isAnnotationOverlay) return;
         (e as any).handlePan = true;
 
-        if (!this.layoutDoc._noAutoscroll && !this.props.renderDepth && this._marqueeRef?.current) {
+        if (!this.layoutDoc._noAutoscroll && !this.props.renderDepth && this._marqueeRef) {
             const dragX = e.detail.clientX;
             const dragY = e.detail.clientY;
-            const bounds = this._marqueeRef.current?.getBoundingClientRect();
+            const bounds = this._marqueeRef?.getBoundingClientRect();
 
             const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0;
             const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0;
@@ -1842,10 +1839,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         DragManager.SetSnapLines(horizLines, vertLines);
     };
 
-    onPointerOver = (e: React.PointerEvent) => {
-        e.stopPropagation();
-    };
-
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
             const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
@@ -1889,7 +1882,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 getContainerTransform={this.getContainerTransform}
                 getTransform={this.getTransform}
                 isAnnotationOverlay={this.isAnnotationOverlay}>
-                <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}>
+                <div
+                    className="marqueeView-div"
+                    ref={r => {
+                        this._marqueeRef = r;
+                        r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
+                    }}
+                    style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}>
                     {this.layoutDoc._backgroundGridShow ? (
                         <div>
                             <CollectionFreeFormBackgroundGrid // bcz : UGHH  don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!?
@@ -1965,7 +1964,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             <div
                 className={'collectionfreeformview-container'}
                 ref={this.createDashEventsTarget}
-                onPointerOver={this.onPointerOver}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
                 onPointerDown={this.onPointerDown}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d3858c25e..fecbc2de2 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -15,7 +15,7 @@ import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../.
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -956,60 +956,58 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
             }
             !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
-
-            const help = cm.findByDescription('Help...');
-            const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
-            helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' });
-            !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' });
-            !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
-            !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
-            
-            let documentationDescription: string | undefined = undefined;
-            let documentationLink: string | undefined = undefined;
-            console.log("type: ", this.props.Document.type);
-            switch(this.props.Document.type) {
-                case DocumentType.COL:
-                    documentationDescription = "See collection documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/views/";
-                    break;
-                case DocumentType.PDF:
-                    documentationDescription = "See PDF node documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/documents/pdf/";
-                    break;
-                case DocumentType.VID:
-                    documentationDescription = "See video node documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/documents/tempMedia/video";
-                    break;
-                case DocumentType.AUDIO:
-                    documentationDescription = "See audio node documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/documents/tempMedia/audio";
-                    break;
-                case DocumentType.WEB:
-                    documentationDescription = "See webpage node documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/documents/webpage/";
-                    break;
-                case DocumentType.IMG:
-                    documentationDescription = "See image node documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/documents/images/";
-                    break;
-                case DocumentType.RTF:
-                    documentationDescription = "See text node documentation";
-                    documentationLink = "https://brown-dash.github.io/Dash-Documentation/documents/text/";
-                    break;
-                default:
-                    break;
-            }
-
-            // Add link to help documentation
-            if (documentationDescription && documentationLink) {
-                console.log("add documentation item");
-                helpItems.push({ description: documentationDescription, event: () => {
+        }
+        const help = cm.findByDescription('Help...');
+        const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+        helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' });
+        !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' });
+        !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
+        !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
+
+        let documentationDescription: string | undefined = undefined;
+        let documentationLink: string | undefined = undefined;
+        switch (this.props.Document.type) {
+            case DocumentType.COL:
+                documentationDescription = 'See collection documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/views/';
+                break;
+            case DocumentType.PDF:
+                documentationDescription = 'See PDF node documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/pdf/';
+                break;
+            case DocumentType.VID:
+                documentationDescription = 'See video node documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/tempMedia/video';
+                break;
+            case DocumentType.AUDIO:
+                documentationDescription = 'See audio node documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/tempMedia/audio';
+                break;
+            case DocumentType.WEB:
+                documentationDescription = 'See webpage node documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/webpage/';
+                break;
+            case DocumentType.IMG:
+                documentationDescription = 'See image node documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/images/';
+                break;
+            case DocumentType.RTF:
+                documentationDescription = 'See text node documentation';
+                documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/text/';
+                break;
+        }
+        // Add link to help documentation
+        if (documentationDescription && documentationLink) {
+            console.log('add documentation item');
+            helpItems.push({
+                description: documentationDescription,
+                event: () => {
                     window.open(documentationLink, '_blank');
-                }, icon: 'book' })
-            };
-
-            cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+                },
+                icon: 'book',
+            });
         }
+        cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
 
         if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
         cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
@@ -1411,29 +1409,23 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     };
     render() {
         TraceMobx();
-        const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : Doc.DocBrushStatus.unbrushed) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
-        const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
-        const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
-        const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
-        let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
-        highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== '[pres element template]'; // bcz: hack to turn off highlighting onsidebar panel documents.  need to flag a document as not highlightable in a more direct way
-
+        const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
         const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
-        const boxShadow = this.props.treeViewDoc
-            ? null
-            : highlighting && this.borderRounding && highlightStyle !== 'dashed'
-            ? `0 0 0 ${highlightIndex}px ${highlightColor}`
-            : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
+        const boxShadow =
+            this.props.treeViewDoc || !highlighting
+                ? null
+                : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed'
+                ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}`
+                : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
         const renderDoc = this.renderDoc({
             borderRadius: this.borderRounding,
-            outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px',
-            border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
+            outline: highlighting && !this.borderRounding && highlighting ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
+            border: highlighting && this.borderRounding && highlighting && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined,
             boxShadow,
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
         const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc;
-        // Return surrounding highlight
         return (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
@@ -1442,8 +1434,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 onKeyDown={this.onKeyDown}
                 onPointerDown={this.onPointerDown}
                 onClick={this.onClick}
-                onPointerEnter={action(e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))}
-                onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))}
+                onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
+                onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
+                onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)}
                 style={{
                     display: this.hidden ? 'inline' : undefined,
                     borderRadius: this.borderRounding,
@@ -1453,9 +1446,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     animRenderDoc
                 ) : (
                     <>
-                        {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}>
-                        {animRenderDoc}
-                    </div> */}
                         {animRenderDoc}
                         <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
                             <svg style={{ overflow: 'visible' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a94e2c96d..f5d00c2b8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -538,6 +538,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     docid: target[Id],
                     float: 'unset',
                 });
+                if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) {
+                    dragData.removeDocument?.(dragData.draggedDocuments[0]);
+                }
                 const view = this._editorView!;
                 view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
                 e.stopPropagation();
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0ceaff968..460dd6081 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -680,6 +680,11 @@ export namespace Doc {
         return alias;
     }
 
+    export function BestAlias(doc: Doc) {
+        const bestAlias = Doc.GetProto(doc) ? DocListCast(doc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : doc;
+        return bestAlias ?? Doc.MakeAlias(doc);
+    }
+
     export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
-- 
cgit v1.2.3-70-g09d2


From 2fc88a931cb2fc3408297b000208990633445585 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 27 Oct 2022 15:57:09 -0400
Subject: cleaned up treeViews to work across PresBox/ppt slides/collections/ -
 modified DocumentView rendering to remove pres box refs.  fixed mainView
 width.

---
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/CurrentUserUtils.ts                |   8 +-
 src/client/views/DocumentDecorations.tsx           |  18 +-
 src/client/views/LightboxView.tsx                  |  26 +--
 src/client/views/MainView.tsx                      |   1 +
 src/client/views/StyleProvider.tsx                 |   5 +-
 .../views/collections/CollectionStackingView.tsx   |   2 +
 .../views/collections/CollectionTreeView.scss      |   2 -
 .../views/collections/CollectionTreeView.tsx       |  62 ++++---
 src/client/views/collections/CollectionView.tsx    |   2 +
 src/client/views/collections/TabDocView.tsx        |   1 -
 src/client/views/collections/TreeView.scss         |   3 +-
 src/client/views/collections/TreeView.tsx          | 179 +++++++++----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   6 +-
 src/client/views/nodes/DocumentView.tsx            |  37 ++--
 src/client/views/nodes/FieldView.tsx               |   2 -
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          |   7 +-
 src/client/views/nodes/trails/PresElementBox.scss  |  14 +-
 src/client/views/nodes/trails/PresElementBox.tsx   | 197 ++++++++++++---------
 src/fields/Doc.ts                                  |   3 +
 23 files changed, 301 insertions(+), 290 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2d82ffdf9..bbb896d6e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -335,7 +335,6 @@ export class DocumentOptions {
     treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)
     treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
     treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
-    treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically
     treeViewChildDoubleClick?: ScriptField; //
     // Action Button
     buttonMenu?: boolean; // whether a action button should be displayed
@@ -395,7 +394,6 @@ export namespace Docs {
                         _xMargin: 10,
                         _yMargin: 10,
                         nativeDimModifiable: true,
-                        treeViewGrowsHorizontally: true,
                         nativeHeightUnfrozen: true,
                         forceReflow: true,
                         links: '@links(self)',
@@ -570,7 +568,7 @@ export namespace Docs {
                 DocumentType.SLIDER,
                 {
                     layout: { view: SliderBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)', treeViewGrowsHorizontally: true },
+                    options: { links: '@links(self)' },
                 },
             ],
             [
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2bc464127..a2974177e 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -480,7 +480,7 @@ export class CurrentUserUtils {
         const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
         const reqdOpts:DocumentOptions = {
             title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true,
-            targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true,  treeViewTruncateTitleWidth: 150, ignoreClick: true, 
+            targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true,  treeViewTruncateTitleWidth: 350, ignoreClick: true, 
             buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias",
             _showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
             contextMenuLabels:new List<string>(contextMenuLabels),
@@ -518,7 +518,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, 
             title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true,
             isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true, 
-            treeViewTruncateTitleWidth: 150, ignoreClick: true, childDropAction: "alias",
+            treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "alias",
             childContextMenuLabels: new List<string>(["Create new folder"]),
             childContextMenuIcons: new List<string>(["plus"]),
             explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
@@ -535,7 +535,7 @@ export class CurrentUserUtils {
     static setupRecentlyClosed(doc: Doc, field:string) {
         const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, 
             title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true,
-            treeViewTruncateTitleWidth: 150, ignoreClick: true,  boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
+            treeViewTruncateTitleWidth: 350, ignoreClick: true,  boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
             contextMenuLabels: new List<string>(["Empty recently closed"]),
             contextMenuIcons:new List<string>(["trash"]),
             explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -561,7 +561,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view",
             boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true,
-            treeViewHideTitle: true, treeViewTruncateTitleWidth: 150
+            treeViewHideTitle: true, treeViewTruncateTitleWidth: 350
         };
         if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]);
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 10517d829..06eb6c6d7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -112,7 +112,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             }
                         }
                         //@ts-ignore
-                        const titleField = +this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
+                        const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
                         Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
 
                         if (d.rootDoc.syncLayoutFieldWithTitle) {
@@ -345,7 +345,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
         const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]);
         const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
-        const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI);
+        const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI);
         seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width);
         seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height);
     };
@@ -381,7 +381,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
         const seldocview = SelectionManager.Views()[0];
         SelectionManager.Views().forEach(dv => {
-            const accumRot = (NumCast(dv.rootDoc._jitterRotation) / 180) * Math.PI;
+            const accumRot = (NumCast(dv.rootDoc._rotation) / 180) * Math.PI;
             const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y);
             const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2];
             const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot);
@@ -396,7 +396,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle });
                     dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x);
                     dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y);
-                    dv.rootDoc._jitterRotation = ((isAbs ? 0 : NumCast(dv.rootDoc._jitterRotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
+                    dv.rootDoc._rotation = ((isAbs ? 0 : NumCast(dv.rootDoc._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
                 })
             );
         };
@@ -416,7 +416,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 return false;
             }, // moveEvent
             action(() => {
-                const oldRotation = NumCast(seldocview.rootDoc._jitterRotation);
+                const oldRotation = NumCast(seldocview.rootDoc._rotation);
                 const diff = oldRotation - Math.round(oldRotation / 45) * 45;
                 if (Math.abs(diff) < 5) {
                     if (selectedInk.length) {
@@ -611,7 +611,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         }
                     } else {
                         const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
-                        const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._jitterRotation) / 180) * Math.PI);
+                        const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI);
 
                         const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
                         dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
@@ -619,7 +619,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         dH && (doc._autoHeight = false);
 
                         const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
-                        const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._jitterRotation) / 180) * Math.PI);
+                        const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._rotation) / 180) * Math.PI);
                         doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc
                         doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]);
                     }
@@ -695,7 +695,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             const loccenter = Utils.rotPt(
                 NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width),
                 NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height),
-                (NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI
+                (NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI
             );
             return seldocview.props
                 .ScreenToLocalTransform()
@@ -783,7 +783,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
         const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
-        const rotation = NumCast(seldocview.rootDoc._jitterRotation);
+        const rotation = NumCast(seldocview.rootDoc._rotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
 
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index bd6cea28a..22b0380a2 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -289,29 +289,29 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                             })}
                             Document={LightboxView.LightboxDoc}
                             DataDoc={undefined}
+                            PanelWidth={this.lightboxWidth}
+                            PanelHeight={this.lightboxHeight}
                             LayoutTemplate={LightboxView.LightboxDocTemplate}
-                            addDocument={undefined}
                             isDocumentActive={returnFalse}
                             isContentActive={returnTrue}
-                            addDocTab={this.addDocTab}
-                            pinToPres={TabDocView.PinDoc}
-                            onBrowseClick={MainView.Instance.exploreMode}
+                            styleProvider={DefaultStyleProvider}
+                            ScreenToLocalTransform={this.lightboxScreenToLocal}
+                            renderDepth={0}
                             rootSelected={returnTrue}
                             docViewPath={returnEmptyDoclist}
                             docFilters={this.docFilters}
-                            removeDocument={undefined}
-                            styleProvider={DefaultStyleProvider}
-                            ScreenToLocalTransform={this.lightboxScreenToLocal}
-                            PanelWidth={this.lightboxWidth}
-                            PanelHeight={this.lightboxHeight}
-                            focus={DocUtils.DefaultFocus}
-                            whenChildContentsActiveChanged={emptyFunction}
-                            bringToFront={emptyFunction}
                             docRangeFilters={returnEmptyFilter}
                             searchFilterDocs={returnEmptyDoclist}
                             ContainingCollectionView={undefined}
                             ContainingCollectionDoc={undefined}
-                            renderDepth={0}
+                            addDocument={undefined}
+                            removeDocument={undefined}
+                            whenChildContentsActiveChanged={emptyFunction}
+                            addDocTab={this.addDocTab}
+                            pinToPres={TabDocView.PinDoc}
+                            bringToFront={emptyFunction}
+                            onBrowseClick={MainView.Instance.exploreMode}
+                            focus={DocUtils.DefaultFocus}
                         />
                     </GestureOverlay>
                 </div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 03c0642ed..4dc1ebd99 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -647,6 +647,7 @@ export class MainView extends React.Component {
                         e.preventDefault();
                     }}
                     style={{
+                        width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
                         minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
                         transform: LightboxView.LightboxDoc ? 'scale(0.0001)' : undefined,
                     }}>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 2eb3dd532..bc8bd7b7f 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -85,7 +85,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
     const isAnnotated = property.includes(':annotated');
     const isOpen = property.includes(':open');
     const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : '';
-    const comicStyle = () => doc && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic';
     const isBackground = () => doc && BoolCast(doc._lockedPosition);
     const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
     const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
@@ -167,11 +166,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.TitleHeight:
             return 15;
         case StyleProp.BorderPath:
-            return comicStyle() && props?.renderDepth && doc?.type !== DocumentType.INK
+            return Doc.IsComicStyle(doc) && props?.renderDepth && doc?.type !== DocumentType.INK
                 ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }
                 : { path: undefined, width: 0 };
         case StyleProp.JitterRotation:
-            return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
+            return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
         case StyleProp.HeaderMargin:
             return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
                 (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) ||
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ba29b1d6f..77b47ed82 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -350,6 +350,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 searchFilterDocs={this.searchFilterDocs}
                 ContainingCollectionDoc={this.props.CollectionView?.props.Document}
                 ContainingCollectionView={this.props.CollectionView}
+                xPadding={NumCast(this.layoutDoc._childXPadding, this.props.childXPadding)}
+                yPadding={NumCast(this.layoutDoc._childYPadding, this.props.childYPadding)}
                 addDocument={this.props.addDocument}
                 moveDocument={this.props.moveDocument}
                 removeDocument={this.props.removeDocument}
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 1aef29b2f..3785b7d61 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -13,8 +13,6 @@
     width: 100%;
     position: relative;
     top: 0;
-    padding-left: 10px;
-    padding-right: 10px;
     background: $light-gray;
     font-size: 13px;
     overflow: auto;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 1169f692a..0ff89c5a7 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -58,7 +58,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     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 monitoring tree view items.
-    private static expandViewLabelSize = 20;
 
     @computed get doc() {
         return this.props.Document;
@@ -100,7 +99,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
+    shrinkWrap = () => {}; // placeholder to allow setContentView to work
+
     componentDidMount() {
+        //this.props.setContentView?.(this);
         this._disposers.autoheight = reaction(
             () => this.rootDoc.autoHeight,
             auto => auto && this.computeHeight(),
@@ -197,9 +199,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     onTreeDrop = (e: React.DragEvent, addDocs?: (docs: Doc[]) => void) => this.onExternalDrop(e, {}, addDocs);
 
     @undoBatch
-    makeTextCollection = (childDocs: Doc[]) => {
-        this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
-    };
+    makeTextCollection = (childDocs: Doc[]) => this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
 
     get editableTitle() {
         return (
@@ -258,6 +258,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         const icons = StrListCast(this.doc.childContextMenuIcons);
         return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
     };
+    headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields);
     @computed get treeViewElements() {
         TraceMobx();
         const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
@@ -281,7 +282,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             this.isContentActive,
             this.panelWidth,
             this.props.renderDepth,
-            () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields),
+            this.headerFields,
             [],
             this.props.onCheckedClick,
             this.onChildClick,
@@ -304,7 +305,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                 className="collectionTreeView-titleBar"
                 ref={action((r: any) => (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.props.ScreenToLocalTransform().Scale))}
                 key={this.doc[Id]}
-                style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}>
+                style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}>
                 {this.outlineMode ? this.documentTitle : this.editableTitle}
             </div>
         );
@@ -372,7 +373,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
     truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
     onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
-    panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.NativeDimScaling?.() || 1);
+    panelWidth = () => Math.max(0, this.props.PanelWidth() - 2 * this.marginX() * (this.props.NativeDimScaling?.() || 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;
@@ -396,7 +397,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                     className="collectionTreeView-contents"
                     key="tree"
                     style={{
-                        ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+                        ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
                         overflow: 'auto',
                         width: '100%',
                         height: '100%',
@@ -405,9 +406,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                     <div
                         className="collectionTreeView-container"
                         style={{
-                            transform: this.outlineMode ? `scale(${this.nativeDimScaling})` : '',
-                            paddingLeft: `${this.marginX()}px`,
-                            width: this.outlineMode ? `calc(${100 / this.nativeDimScaling}%)` : '',
+                            marginLeft: `${this.marginX()}px`,
                             minHeight: `calc(100% - ${this._titleHeight}px)`,
                         }}
                         onContextMenu={this.onContextMenu}>
@@ -432,24 +431,29 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     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()
+        const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1) || 1;
+        return (
+            <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
+                {!(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()
+                )}
+            </div>
         );
     }
 }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index dcaad5632..9f63a11aa 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -54,6 +54,8 @@ interface CollectionViewProps_ extends FieldViewProps {
     childHideDecorationTitle?: () => boolean;
     childHideResizeHandles?: () => boolean;
     childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
+    childXPadding?: number;
+    childYPadding?: number;
     childLayoutString?: string;
     childIgnoreNativeSize?: boolean;
     childClickScript?: ScriptField;
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index a6cfbd6b2..cde5132a3 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -256,7 +256,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
             pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
             pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
-            pinDoc.treeViewGrowsHorizontally = true; // the document expands horizontally when displayed as a tree view header
             pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
             const presArray: Doc[] = PresBox.Instance?.sortArray();
             const size: number = PresBox.Instance?.selectedArray.size;
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 57bb5274d..83fee013a 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -24,6 +24,7 @@
         // width: $TREE_BULLET_WIDTH;
         width: 100%;
         height: 100%;
+        position: absolute;
 
         .treeView-expandIcon {
             display: none;
@@ -118,7 +119,7 @@
     display: flex; // needed for PresBox's treeView
     border: transparent 1px solid;
     align-items: center;
-    width: 100%;
+    width: max-content;
     border-radius: 5px;
 
     &:hover {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index b9f86fa6b..1e97eee37 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -10,7 +10,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -321,6 +321,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             _viewType: CollectionViewType.Tree,
             hideLinkButton: true,
             _showSidebar: true,
+            _fitWidth: true,
             treeViewType: TreeViewType.outline,
             x: 0,
             y: 0,
@@ -398,29 +399,22 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
     docTransform = () => this.refTransform(this._dref?.ContentRef?.current);
     getTransform = () => this.refTransform(this._tref.current);
-    docWidth = () => {
-        const layoutDoc = this.layoutDoc;
-        const aspect = Doc.NativeAspect(layoutDoc);
-        if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]());
-        if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth()));
-        return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]());
-    };
-    docHeight = () => {
-        const layoutDoc = this.layoutDoc;
-        return Math.max(
-            70,
-            Math.min(
-                this.MAX_EMBED_HEIGHT,
-                (() => {
-                    const aspect = Doc.NativeAspect(layoutDoc);
-                    if (aspect) return this.docWidth() / (aspect || 1);
-                    return layoutDoc._fitWidth
-                        ? !Doc.NativeHeight(this.doc)
-                            ? NumCast(this.props.containerCollection._height)
-                            : Math.min((this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height)))
-                        : layoutDoc[HeightSym]() || 50;
-                })()
-            )
+    embeddedPanelWidth = () => this.props.panelWidth() / (this.props.treeView.props.NativeDimScaling?.() || 1);
+    embeddedPanelHeight = () => {
+        console.log(this.props.treeView.rootDoc.title);
+        const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
+        return Math.min(
+            layoutDoc[HeightSym](),
+            this.MAX_EMBED_HEIGHT,
+            (() => {
+                const aspect = Doc.NativeAspect(layoutDoc);
+                if (aspect) return this.embeddedPanelWidth() / (aspect || 1);
+                return layoutDoc._fitWidth
+                    ? !Doc.NativeHeight(layoutDoc)
+                        ? NumCast(layoutDoc._height) //this.props.containerCollection._height)
+                        : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height)))
+                    : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym]();
+            })()
         );
     };
 
@@ -512,28 +506,6 @@ export class TreeView extends React.Component<TreeViewProps> {
         return rows;
     }
 
-    rtfWidth = () => {
-        const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
-        return Math.min(layout[WidthSym](), this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.NativeDimScaling?.() || 1);
-    };
-    rtfHeight = () => {
-        const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
-        return this.rtfWidth() <= layout[WidthSym]() ? Math.min(layout[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
-    };
-    rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth());
-    expandPanelHeight = () => {
-        if (this.layoutDoc._fitWidth) return this.docHeight();
-        const aspect = this.layoutDoc[WidthSym]() / this.layoutDoc[HeightSym]();
-        const docAspect = this.docWidth() / this.docHeight();
-        return docAspect < aspect ? this.docWidth() / aspect : this.docHeight();
-    };
-    expandPanelWidth = () => {
-        if (this.layoutDoc._fitWidth) return this.docWidth();
-        const aspect = this.layoutDoc[WidthSym]() / this.layoutDoc[HeightSym]();
-        const docAspect = this.docWidth() / this.docHeight();
-        return docAspect > aspect ? this.docHeight() * aspect : this.docWidth();
-    };
-
     @computed get renderContent() {
         TraceMobx();
         const expandKey = this.treeViewExpandedView;
@@ -792,15 +764,18 @@ export class TreeView extends React.Component<TreeViewProps> {
     titleStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (!doc || doc !== this.doc) return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
 
+        const treeView = this.props.treeView;
         // prettier-ignore
         switch (property.split(':')[0]) {
             case StyleProp.Opacity:         return this.props.treeView.outlineMode ? undefined : 1;
             case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
             case StyleProp.Highlighting:    if (this.props.treeView.outlineMode) return undefined;
+            case StyleProp.Hidden:          return false;
+            case StyleProp.BoxShadow:       return undefined;
             case StyleProp.DocContents:
                 const highlightIndex = this.props.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.isBrushedHighlightedDegree(doc);
                 const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
-                return this.props.treeView.outlineMode ? null : (
+                return treeView.outlineMode ? null : (
                     <div
                         className="treeView-label"
                         style={{
@@ -808,15 +783,20 @@ export class TreeView extends React.Component<TreeViewProps> {
                             maxWidth: props?.PanelWidth() || undefined,
                             background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor),
                             outline: `solid ${highlightColor} ${highlightIndex}px`,
+                            paddingLeft: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)),
+                            paddingRight: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)),
+                            paddingTop: treeView.props.childYPadding,
+                            paddingBottom: treeView.props.childYPadding,
                         }}>
                         {StrCast(doc?.title)}
                     </div>
                 );
         }
-        return this.props?.treeView?.props.styleProvider?.(doc, props, property);
+        return treeView.props.styleProvider?.(doc, props, property);
     };
     embeddedStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (property.startsWith(StyleProp.Decorations)) return null;
+        if (property.startsWith(StyleProp.Hidden)) return false;
         return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
     };
     onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
@@ -843,7 +823,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         }
         return false;
     };
-    titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth()));
+    titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - 3 * treeBulletWidth();
 
     return18 = () => 18;
     /**
@@ -851,6 +831,7 @@ export class TreeView extends React.Component<TreeViewProps> {
      */
     @computed
     get renderTitle() {
+        //
         TraceMobx();
         const view = this._editTitle ? (
             <EditableView
@@ -887,6 +868,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                     }
                 })}
                 Document={this.doc}
+                fitWidth={(doc: Doc) => true}
                 DataDoc={undefined}
                 scriptContext={this}
                 hideDecorationTitle={this.props.treeView.outlineMode}
@@ -905,7 +887,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 removeDocument={this.props.removeDoc}
                 ScreenToLocalTransform={this.getTransform}
                 NativeHeight={this.return18}
-                NativeWidth={this.titleWidth}
+                NativeWidth={returnZero}
                 PanelWidth={this.titleWidth}
                 PanelHeight={this.return18}
                 contextMenuItems={this.contextMenuItems}
@@ -918,10 +900,12 @@ export class TreeView extends React.Component<TreeViewProps> {
                 disableDocBrushing={this.props.treeView.props.disableDocBrushing}
                 hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
                 dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
+                xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)}
+                yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)}
                 docFilters={returnEmptyFilter}
                 docRangeFilters={returnEmptyFilter}
                 searchFilterDocs={returnEmptyDoclist}
-                ContainingCollectionView={undefined}
+                ContainingCollectionView={this.props.treeView.props.CollectionView}
                 ContainingCollectionDoc={this.props.treeView.props.Document}
             />
         );
@@ -968,53 +952,52 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
 
     renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
-        const isExpandable = this.doc._treeViewGrowsHorizontally;
-        const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
-        const panelHeight = asText ? this.rtfOutlineHeight : isExpandable ? this.rtfHeight : this.expandPanelHeight;
         return (
-            <DocumentView
-                key={this.doc[Id]}
-                ref={action((r: DocumentView | null) => (this._dref = r))}
-                Document={this.doc}
-                DataDoc={undefined}
-                PanelWidth={panelWidth}
-                PanelHeight={panelHeight}
-                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}
-                LayoutTemplate={this.props.treeView.props.childLayoutTemplate}
-                isContentActive={isActive}
-                isDocumentActive={isActive}
-                styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
-                hideTitle={asText}
-                fitContentsToBox={returnTrue}
-                hideDecorationTitle={this.props.treeView.outlineMode}
-                hideResizeHandles={this.props.treeView.outlineMode}
-                onClick={this.onChildClick}
-                focus={this.refocus}
-                onKey={this.onKeyDown}
-                hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
-                dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
-                ScreenToLocalTransform={this.docTransform}
-                renderDepth={this.props.renderDepth + 1}
-                treeViewDoc={this.props.treeView?.props.Document}
-                rootSelected={returnTrue}
-                docViewPath={this.props.treeView.props.docViewPath}
-                docFilters={returnEmptyFilter}
-                docRangeFilters={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-                ContainingCollectionDoc={this.props.containerCollection}
-                ContainingCollectionView={undefined}
-                addDocument={this.props.addDocument}
-                moveDocument={this.move}
-                removeDocument={this.props.removeDoc}
-                whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
-                addDocTab={this.props.addDocTab}
-                pinToPres={this.props.treeView.props.pinToPres}
-                disableDocBrushing={this.props.treeView.props.disableDocBrushing}
-                bringToFront={returnFalse}
-                scriptContext={this}
-            />
+            <div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
+                <DocumentView
+                    key={this.doc[Id]}
+                    ref={action((r: DocumentView | null) => (this._dref = r))}
+                    Document={this.doc}
+                    DataDoc={undefined}
+                    PanelWidth={this.embeddedPanelWidth}
+                    PanelHeight={this.embeddedPanelHeight}
+                    LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
+                    LayoutTemplate={this.props.treeView.props.childLayoutTemplate}
+                    isContentActive={isActive}
+                    isDocumentActive={isActive}
+                    styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
+                    hideTitle={asText}
+                    //fitContentsToBox={returnTrue}
+                    hideDecorationTitle={this.props.treeView.outlineMode}
+                    hideResizeHandles={this.props.treeView.outlineMode}
+                    onClick={this.onChildClick}
+                    focus={this.refocus}
+                    onKey={this.onKeyDown}
+                    hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
+                    dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
+                    ScreenToLocalTransform={this.docTransform}
+                    renderDepth={this.props.renderDepth + 1}
+                    treeViewDoc={this.props.treeView?.props.Document}
+                    rootSelected={returnTrue}
+                    docViewPath={this.props.treeView.props.docViewPath}
+                    docFilters={returnEmptyFilter}
+                    docRangeFilters={returnEmptyFilter}
+                    searchFilterDocs={returnEmptyDoclist}
+                    ContainingCollectionDoc={this.props.containerCollection}
+                    ContainingCollectionView={undefined}
+                    addDocument={this.props.addDocument}
+                    moveDocument={this.move}
+                    removeDocument={this.props.removeDoc}
+                    whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+                    xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)}
+                    yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)}
+                    addDocTab={this.props.addDocTab}
+                    pinToPres={this.props.treeView.props.pinToPres}
+                    disableDocBrushing={this.props.treeView.props.disableDocBrushing}
+                    bringToFront={returnFalse}
+                    scriptContext={this}
+                />
+            </div>
         );
     };
 
@@ -1148,7 +1131,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         }
 
         const docs = TreeView.sortDocs(childDocs, StrCast(containerCollection.treeViewSortCriterion, TreeSort.None));
-        const rowWidth = () => panelWidth() - treeBulletWidth();
+        const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView.props.NativeDimScaling?.() || 1);
         const treeViewRefs = new Map<Doc, TreeView | undefined>();
         return docs
             .filter(child => child instanceof Doc)
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 559951efc..2f246e74f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1317,7 +1317,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 dontScaleFilter={this.props.dontScaleFilter}
                 dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
                 pointerEvents={this.pointerEvents}
-                jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
+                rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
                 //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
             />
         );
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 04c7b96e3..260c98816 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -25,7 +25,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     renderCutoffProvider: (doc: Doc) => boolean;
     zIndex?: number;
     highlight?: boolean;
-    jitterRotation: number;
+    rotation: number;
     dataTransition?: string;
     replica: string;
     CollectionFreeFormView: CollectionFreeFormView;
@@ -38,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         { key: '_width' },
         { key: 'x' },
         { key: 'y' },
-        { key: '_jitterRotation', val: 0 },
+        { key: '_rotation', val: 0 },
         { key: '_scrollTop' },
         { key: 'opacity', val: 1 },
         { key: 'viewScale', val: 1 },
@@ -55,7 +55,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     get transform() {
-        return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`;
+        return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.rotation, this.props.rotation)}deg)`;
     }
     get X() {
         return this.dataProvider?.x ?? NumCast(this.Document.x);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fecbc2de2..5b26469ed 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -53,6 +53,7 @@ import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PresBox } from './trails/PresBox';
 import React = require('react');
+import { CollectionTreeView } from '../collections/CollectionTreeView';
 const { Howl } = require('howler');
 
 interface Window {
@@ -147,6 +148,8 @@ export interface DocumentViewSharedProps {
     pinToPres: (document: Doc) => void;
     ScreenToLocalTransform: () => Transform;
     bringToFront: (doc: Doc, sendToBack?: boolean) => void;
+    xPadding?: number;
+    yPadding?: number;
     dropAction?: dropActionType;
     dontRegisterView?: boolean;
     hideLinkButton?: boolean;
@@ -1349,17 +1352,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             this.contents
         ) : (
             <div className="documentView-styleWrapper">
-                {!this.headerMargin ? (
-                    <>
-                        {' '}
-                        {this.contents} {titleView}{' '}
-                    </>
-                ) : (
-                    <>
-                        {' '}
-                        {titleView} {this.contents}{' '}
-                    </>
-                )}
+                {' '}
+                {!this.headerMargin ? this.contents : titleView}
+                {!this.headerMargin ? titleView : this.contents}
+                {' ' /* */}
                 {captionView}
             </div>
         );
@@ -1372,7 +1368,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         TraceMobx();
         const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
         const isButton = this.props.Document.type === DocumentType.FONTICON;
-        if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null;
+        if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
         return (
             this.docContents ?? (
                 <div
@@ -1448,7 +1444,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     <>
                         {animRenderDoc}
                         <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
-                            <svg style={{ overflow: 'visible' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
+                            <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
                                 <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
                             </svg>
                         </div>
@@ -1552,7 +1548,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
     }
     @computed get shouldNotScale() {
-        return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+        return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [CollectionViewType.Docking].includes(this.Document._viewType as any);
     }
     @computed get effectiveNativeWidth() {
         return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
@@ -1597,7 +1593,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
     focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options);
     getBounds = () => {
-        if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+        if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
             return undefined;
         }
         const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse();
@@ -1687,10 +1683,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     render() {
         TraceMobx();
-        const xshift = () => (Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
-        const yshift = () => (Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
-        const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES;
-        const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
+        const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
+        const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
+        const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
         return (
             <div className="contentFittingDocumentView">
                 {!this.props.Document || !this.props.PanelWidth() ? null : (
@@ -1700,11 +1695,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         style={{
                             transition: this.props.dataTransition,
                             transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
-                            width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
+                            width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
                             height:
                                 isButton || this.props.forceAutoHeight
                                     ? undefined
-                                    : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
+                                    : 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 77aaa4441..e53422ab7 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -36,8 +36,6 @@ export interface FieldViewProps extends DocumentViewSharedProps {
     fontSize?: number;
     height?: number;
     width?: number;
-    xPadding?: number;
-    yPadding?: number;
     noSidebar?: boolean;
     dontScale?: boolean;
     dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field)
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index f7f558bb4..fb47bfc07 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -935,7 +935,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     className="videoBox-ui"
                     style={{
                         transformOrigin: 'top left',
-                        transform: `rotate(${NumCast(this.rootDoc.jitterRotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
+                        transform: `rotate(${NumCast(this.rootDoc.rotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
                         left: xPos,
                         top: yMid,
                         height: uiHeight,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 5470d6ba9..096f9a92c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1004,11 +1004,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             () => this.autoHeight,
             autoHeight => autoHeight && this.tryUpdateScrollHeight()
         );
+        this._disposers.width = reaction(
+            () => this.props.PanelWidth(),
+            width => this.tryUpdateScrollHeight()
+        );
         this._disposers.scrollHeight = reaction(
             () => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }),
-            ({ width, scrollHeight, autoHeight }) => {
-                width && autoHeight && this.resetNativeHeight(scrollHeight);
-            },
+            ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight),
             { fireImmediately: true }
         );
         this._disposers.componentHeights = reaction(
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 258dad39c..4e8aed8a6 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -429,7 +429,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presPinLayout = true;
             pinDoc.presX = NumCast(targetDoc.x);
             pinDoc.presY = NumCast(targetDoc.y);
-            pinDoc.presRot = NumCast(targetDoc.jitterRotation);
+            pinDoc.presRot = NumCast(targetDoc.rotation);
             pinDoc.presWidth = NumCast(targetDoc.width);
             pinDoc.presHeight = NumCast(targetDoc.height);
         }
@@ -521,7 +521,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             targetDoc._dataTransition = presTransitionTime;
             targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x));
             targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y));
-            targetDoc.jitterRotation = NumCast(activeItem.presRot, NumCast(targetDoc.jitterRotation));
+            targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation));
             targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width));
             targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height));
             setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10);
@@ -2652,9 +2652,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 PanelHeight={this.panelHeight}
                                 childIgnoreNativeSize={true}
                                 moveDocument={returnFalse}
-                                childFitWidth={returnTrue}
+                                //childFitWidth={returnTrue}
                                 childOpacity={returnOne}
                                 childLayoutTemplate={this.childLayoutTemplate}
+                                childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
                                 filterAddDocument={this.addDocumentFilter}
                                 removeDocument={returnFalse}
                                 dontRegisterView={true}
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 8a6c2a6dc..415253af1 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -42,10 +42,7 @@ $slide-active: #5b9fdd;
     width: 100%;
     border-bottom: 0.5px solid grey;
     display: flex;
-    justify-content: space-between;
     align-items: center;
-    grid-template-rows: 16px 10px auto;
-    grid-template-columns: max-content max-content max-content max-content auto;
 
     .presItem-name {
         display: flex;
@@ -102,13 +99,7 @@ $slide-active: #5b9fdd;
         grid-row: 3;
         grid-column: 1/8;
         position: relative;
-        display: flex;
-        width: auto;
-        justify-content: center;
-        margin: auto;
-        margin-bottom: 2px;
-        border-bottom-left-radius: 5px;
-        border-bottom-right-radius: 5px;
+        display: inline-block;
     }
 
     .presItem-embeddedMask {
@@ -124,8 +115,7 @@ $slide-active: #5b9fdd;
 
     .presItem-slideButtons {
         display: flex;
-        grid-column: 7;
-        grid-row: 1/3;
+        position: absolute;
         width: max-content;
         justify-self: right;
         justify-content: flex-end;
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index f4ab845f3..5d14a0e9a 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -4,7 +4,9 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'
 import { observer } from 'mobx-react';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
+import { InkField } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
+import { RichTextField } from '../../../../fields/RichTextField';
 import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
 import { Docs, DocUtils } from '../../../documents/Documents';
@@ -14,6 +16,7 @@ import { DragManager } from '../../../util/DragManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
+import { MarqueeView } from '../../collections/collectionFreeForm';
 import { ViewBoxBaseComponent } from '../../DocComponent';
 import { EditableView } from '../../EditableView';
 import { Colors } from '../../global/globalEnums';
@@ -24,9 +27,6 @@ import { PresBox } from './PresBox';
 import './PresElementBox.scss';
 import { PresMovement } from './PresEnums';
 import React = require('react');
-import { InkField } from '../../../../fields/InkField';
-import { RichTextField } from '../../../../fields/RichTextField';
-import { MarqueeView } from '../../collections/collectionFreeForm';
 /**
  * This class models the view a document added to presentation will have in the presentation.
  * It involves some functionality for its buttons and options.
@@ -44,8 +44,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get indexInPres() {
         return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc);
     } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
+    @computed get expandViewHeight() {
+        return 100;
+    }
     @computed get collapsedHeight() {
-        return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31;
+        return 35;
     } // the collapsed height changes depending on the state of the presBox.  We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
     @computed get presStatus() {
         return this.presBox.presStatus;
@@ -58,7 +61,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as PresBox) : undefined;
     }
     @computed get presBox() {
-        return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!;
+        return this.props.ContainingCollectionDoc!;
     }
     @computed get targetDoc() {
         return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc;
@@ -67,8 +70,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this.layoutDoc.hideLinkButton = true;
         this._heightDisposer = reaction(
-            () => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
-            params => (this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0)),
+            () => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }),
+            ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)),
             { fireImmediately: true }
         );
     }
@@ -84,10 +87,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton);
 
-    embedHeight = (): number => 97;
+    embedHeight = (): number => this.collapsedHeight + this.expandViewHeight;
     // embedWidth = () => this.props.PanelWidth();
     // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
-    embedWidth = (): number => this.props.PanelWidth() - 35;
+    embedWidth = (): number => this.props.PanelWidth() / 2;
     styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (property === StyleProp.Opacity) return 1;
         return this.props.styleProvider?.(doc, props, property);
@@ -98,35 +101,35 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @computed get renderEmbeddedInline() {
         return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : (
-            <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
+            <div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}>
                 <DocumentView
                     Document={this.rootDoc}
                     DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+                    PanelWidth={this.embedWidth}
+                    PanelHeight={this.embedHeight}
+                    isContentActive={this.props.isContentActive}
                     styleProvider={this.styleProvider}
+                    hideLinkButton={true}
+                    ScreenToLocalTransform={Transform.Identity}
+                    renderDepth={this.props.renderDepth + 1}
                     docViewPath={returnEmptyDoclist}
+                    docFilters={this.props.docFilters}
+                    docRangeFilters={this.props.docRangeFilters}
+                    searchFilterDocs={this.props.searchFilterDocs}
+                    ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
                     rootSelected={returnTrue}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
-                    isContentActive={this.props.isContentActive}
-                    addDocTab={returnFalse}
-                    pinToPres={returnFalse}
                     fitContentsToBox={returnTrue}
-                    PanelWidth={this.embedWidth}
-                    PanelHeight={this.embedHeight}
-                    ScreenToLocalTransform={Transform.Identity}
                     moveDocument={this.props.moveDocument!}
-                    renderDepth={this.props.renderDepth + 1}
                     focus={DocUtils.DefaultFocus}
                     whenChildContentsActiveChanged={returnFalse}
+                    addDocTab={returnFalse}
+                    pinToPres={returnFalse}
                     bringToFront={returnFalse}
-                    docFilters={this.props.docFilters}
-                    docRangeFilters={this.props.docRangeFilters}
-                    searchFilterDocs={this.props.searchFilterDocs}
-                    ContainingCollectionView={undefined}
-                    ContainingCollectionDoc={undefined}
-                    hideLinkButton={true}
                 />
-                <div className="presItem-embeddedMask" />
+                {/* <div className="presItem-embeddedMask" /> */}
             </div>
         );
     }
@@ -206,8 +209,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []);
         if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
         dragData.dropAction = 'move';
-        dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc;
-        dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument;
+        dragData.treeViewDoc = this.presBox._viewType === CollectionViewType.Tree ? this.props.ContainingCollectionDoc : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
+        dragData.moveDocument = this.props.moveDocument;
         const dragItem: HTMLElement[] = [];
         if (dragArray.length === 1) {
             const doc = this._itemRef.current || dragArray[0];
@@ -307,7 +310,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => {
         activeItem.presX = NumCast(targetDoc.x);
         activeItem.presY = NumCast(targetDoc.y);
-        activeItem.presRot = NumCast(targetDoc.jitterRotation);
+        activeItem.presRot = NumCast(targetDoc.rotation);
         activeItem.presWidth = NumCast(targetDoc.width);
         activeItem.presHeight = NumCast(targetDoc.height);
     };
@@ -464,16 +467,90 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return width;
     }
 
+    @computed get presButtons() {
+        const presBox: Doc = this.presBox; //presBox
+        const presBoxColor: string = StrCast(presBox._backgroundColor);
+        const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
+        const targetDoc: Doc = this.targetDoc;
+        const activeItem: Doc = this.rootDoc;
+
+        const items: JSX.Element[] = [];
+        if (activeItem.presPinLayout) {
+            items.push(
+                <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
+                    <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
+                        L
+                    </div>
+                </Tooltip>
+            );
+        }
+        if (activeItem.presPinData || activeItem.presPinView) {
+            items.push(
+                <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
+                    <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
+                        C
+                    </div>
+                </Tooltip>
+            );
+        }
+        if (!Doc.noviceMode) {
+            items.push(
+                <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
+                    <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
+                        <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
+                    </div>
+                </Tooltip>
+            );
+        }
+        if (this.indexInPres === 0) {
+            items.push(
+                <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
+                    <div
+                        className="slideButton"
+                        onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
+                        style={{
+                            zIndex: 1000 - this.indexInPres,
+                            fontWeight: 700,
+                            backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
+                            height: activeItem.groupWithUp ? 53 : 18,
+                            transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
+                        }}>
+                        <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
+                            <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} />
+                        </div>
+                    </div>
+                </Tooltip>
+            );
+        }
+        items.push(
+            <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}>
+                <div
+                    className="slideButton"
+                    onClick={e => {
+                        e.stopPropagation();
+                        this.presExpandDocumentClick();
+                    }}>
+                    <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} />
+                </div>
+            </Tooltip>
+        );
+        items.push(
+            <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
+                <div className={'slideButton'} onClick={this.removeItem}>
+                    <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
+                </div>
+            </Tooltip>
+        );
+        return items;
+    }
+
     @computed get mainItem() {
         const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false;
         const isCurrent: boolean = this.presBox._itemIndex === this.indexInPres;
-        const toolbarWidth: number = this.toolbarWidth;
-        const showMore: boolean = this.toolbarWidth >= 300;
         const miniView: boolean = this.toolbarWidth <= 110;
         const presBox: Doc = this.presBox; //presBox
         const presBoxColor: string = StrCast(presBox._backgroundColor);
         const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
-        const targetDoc: Doc = this.targetDoc;
         const activeItem: Doc = this.rootDoc;
 
         return (
@@ -484,6 +561,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 style={{
                     backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent',
                     opacity: this._dragging ? 0.3 : 1,
+                    paddingLeft: NumCast(this.layoutDoc._xPadding, this.props.xPadding),
+                    paddingRight: NumCast(this.layoutDoc._xPadding, this.props.xPadding),
+                    paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
+                    paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
                 }}
                 onClick={e => {
                     e.stopPropagation();
@@ -507,6 +588,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         ref={this._dragRef}
                         className={`presItem-slide ${isCurrent ? 'active' : ''}`}
                         style={{
+                            display: 'infline-block',
                             backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
                             //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
                             border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined,
@@ -514,8 +596,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         <div
                             className="presItem-name"
                             style={{
+                                display: 'inline-flex',
                                 pointerEvents: isSelected ? undefined : 'none',
-                                maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105,
+                                width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
                                 cursor: isSelected ? 'text' : 'grab',
                             }}>
                             <div>{`${this.indexInPres + 1}.  `}</div>
@@ -523,56 +606,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </div>
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */}
-                        <div className="presItem-slideButtons">
-                            <Tooltip title={<div className="dash-tooltip">Update captured doc layout</div>}>
-                                <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinLayout ? 'flex' : 'none' }}>
-                                    L
-                                </div>
-                            </Tooltip>
-                            <Tooltip title={<div className="dash-tooltip">Update captured doc content</div>}>
-                                <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinData || activeItem.presPinView ? 'flex' : 'none' }}>
-                                    C
-                                </div>
-                            </Tooltip>
-                            {!Doc.noviceMode && (
-                                <Tooltip title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
-                                    <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
-                                        <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
-                                    </div>
-                                </Tooltip>
-                            )}
-                            <Tooltip title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
-                                <div
-                                    className="slideButton"
-                                    onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
-                                    style={{
-                                        display: this.indexInPres === 0 ? 'none' : '',
-                                        zIndex: 1000 - this.indexInPres,
-                                        fontWeight: 700,
-                                        backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
-                                        height: activeItem.groupWithUp ? 53 : 18,
-                                        transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
-                                    }}>
-                                    <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
-                                        <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} />
-                                    </div>
-                                </div>
-                            </Tooltip>
-                            <Tooltip title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}>
-                                <div
-                                    className="slideButton"
-                                    onClick={e => {
-                                        e.stopPropagation();
-                                        this.presExpandDocumentClick();
-                                    }}>
-                                    <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} />
-                                </div>
-                            </Tooltip>
-                            <Tooltip title={<div className="dash-tooltip">Remove from presentation</div>}>
-                                <div className={'slideButton'} onClick={this.removeItem}>
-                                    <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
-                                </div>
-                            </Tooltip>
+                        <div className="presItem-slideButtons" style={{ position: 'absolute', right: 0 }}>
+                            {...this.presButtons}
                         </div>
                         {this.renderEmbeddedInline}
                     </div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 466b43287..fc43325fe 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -238,6 +238,9 @@ export class Doc extends RefField {
         Doc.UserDoc().activePage = val;
         DocServer.UPDATE_SERVER_CACHE();
     }
+    public static IsComicStyle(doc?: Doc) {
+        return doc && Doc.ActiveDashboard && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic';
+    }
     public static get ActiveDashboard() {
         return DocCast(Doc.UserDoc().activeDashboard);
     }
-- 
cgit v1.2.3-70-g09d2


From dc942e6228e003caa3754a72c0e126d64332a004 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Nov 2022 18:29:11 -0400
Subject: fixes for goldenlayout to initialize more cleanly.  updated all old
 ReactDOM.render() to ReactDom.createRoot().  fixes for PDF/Web sidebar
 sizing.  added text from pdf selection anchors to sidebar notes.   fixed  PDF
 text selection to align properly.

---
 src/client/goldenLayout.js                         |  11 +-
 src/client/util/DragManager.ts                     |   2 +-
 src/client/util/HypothesisUtils.ts                 | 160 ++++++++++++---------
 src/client/views/DocumentDecorations.scss          |   5 +-
 src/client/views/DocumentDecorations.tsx           |   9 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/Main.tsx                          |   7 +-
 src/client/views/MainView.scss                     |   9 --
 src/client/views/MainView.tsx                      |  94 +-----------
 src/client/views/MarqueeAnnotator.tsx              |   6 +-
 src/client/views/SidebarAnnos.tsx                  |  22 +++
 src/client/views/StyleProvider.tsx                 |   6 +-
 .../views/collections/CollectionDockingView.scss   |   4 +
 .../views/collections/CollectionDockingView.tsx    |  17 ++-
 src/client/views/collections/TabDocView.tsx        |   6 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |   3 -
 src/client/views/nodes/PDFBox.tsx                  |  13 +-
 src/client/views/nodes/WebBox.tsx                  |  14 +-
 src/client/views/nodes/button/FontIconBox.tsx      |  12 +-
 .../nodes/formattedText/DashDocCommentView.tsx     |   8 +-
 .../views/nodes/formattedText/DashDocView.tsx      |  12 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |  53 +++++--
 .../views/nodes/formattedText/EquationView.tsx     |  10 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   1 +
 .../views/nodes/formattedText/RichTextRules.ts     |   7 +-
 .../views/nodes/formattedText/SummaryView.tsx      |   9 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |   1 +
 src/client/views/pdf/PDFViewer.scss                |  12 +-
 src/client/views/pdf/PDFViewer.tsx                 |   6 +-
 src/debug/Repl.tsx                                 |  37 +++--
 30 files changed, 278 insertions(+), 280 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index dd11e6466..bc08b4d0b 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -459,7 +459,7 @@
                     this._bDragging = false;
                     this.emit('dragStop', oEvent, this._nOriginalX + this._nX);
                 } else  { // make title receive pointer events to allow setting insertion position or selecting texst range
-                    const classname = typeof oEvent.target?.className === "string" ? oEvent.target.className : "";
+                    const classname = oEvent.target ? (typeof oEvent.target.className === "string" ? oEvent.target.className : ""): "";
                     if (classname.includes("lm_title_wrap")) {
                         oEvent.target.children[0].style.pointerEvents = "all";
                         oEvent.target.children[0].focus();
@@ -5391,10 +5391,11 @@
          * @returns {void}
          */
         _render: function () {
-            this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]);
-            this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () {
+            this._reactComponent = ReactDOM.createRoot(this._container.getElement()[0])
+            this._reactComponent.render(this._getReactComponent());
+            this._originalComponentWillUpdate = this._reactComponent.componentDidUpdate || function () {
             };
-            this._reactComponent.componentWillUpdate = this._onUpdate.bind(this);
+            this._reactComponent.componentDidUpdate = this._onUpdate.bind(this);
             if (this._container.getState()) {
                 this._reactComponent.setState(this._container.getState());
             }
@@ -5407,7 +5408,7 @@
          * @returns {void}
          */
         _destroy: function () {
-            ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
+          //  ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
             this._container.off('open', this._render, this);
             this._container.off('destroy', this._destroy, this);
         },
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 36e5a65fb..d0690fa10 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -450,7 +450,7 @@ export namespace DragManager {
         const hideDragShowOriginalElements = (hide: boolean) => {
             dragLabel.style.display = hide ? '' : 'none';
             !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
-            eles.forEach(ele => (ele.hidden = hide));
+            setTimeout(() => eles.forEach(ele => (ele.hidden = hide)));
         };
         options?.hideSource && hideDragShowOriginalElements(true);
 
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index e910a9118..297520d5d 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -1,30 +1,28 @@
-import { StrCast, Cast } from "../../fields/Types";
-import { SearchUtil } from "./SearchUtil";
-import { action, runInAction } from "mobx";
-import { Doc, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { Docs } from "../documents/Documents";
-import { SelectionManager } from "./SelectionManager";
-import { WebField } from "../../fields/URLField";
-import { DocumentManager } from "./DocumentManager";
-import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton";
-import { simulateMouseClick, Utils } from "../../Utils";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { Id } from "../../fields/FieldSymbols";
+import { StrCast, Cast } from '../../fields/Types';
+import { SearchUtil } from './SearchUtil';
+import { action, runInAction } from 'mobx';
+import { Doc, Opt } from '../../fields/Doc';
+import { DocumentType } from '../documents/DocumentTypes';
+import { Docs } from '../documents/Documents';
+import { SelectionManager } from './SelectionManager';
+import { WebField } from '../../fields/URLField';
+import { DocumentManager } from './DocumentManager';
+import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton';
+import { simulateMouseClick, Utils } from '../../Utils';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { Id } from '../../fields/FieldSymbols';
 
 export namespace Hypothesis {
-
     /**
-     * Retrieve a WebDocument with the given url, prioritizing results that are on screen.  
+     * Retrieve a WebDocument with the given url, prioritizing results that are on screen.
      * If none exist, create and return a new WebDocument.
      */
     export const getSourceWebDoc = async (uri: string) => {
         const result = await findWebDoc(uri);
-        console.log(result ? "existing doc found" : "existing doc NOT found");
+        console.log(result ? 'existing doc found' : 'existing doc NOT found');
         return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found
     };
 
-
     /**
      * Search for a WebDocument whose url field matches the given uri, return undefined if not found
      */
@@ -33,18 +31,18 @@ export namespace Hypothesis {
         if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise
 
         const results: Doc[] = [];
-        await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => {
-            const docs = res.docs;
-            const filteredDocs = docs.filter(doc =>
-                doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data
-            );
-            filteredDocs.forEach(doc => {
-                uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? 
-            });
-        }));
+        await SearchUtil.Search('web', true).then(
+            action(async (res: SearchUtil.DocSearchResult) => {
+                const docs = res.docs;
+                const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data);
+                filteredDocs.forEach(doc => {
+                    uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
+                });
+            })
+        );
 
         const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc));
-        return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen
+        return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen
     };
 
     /**
@@ -52,17 +50,19 @@ export namespace Hypothesis {
      */
     export const linkListener = async (e: any) => {
         const annotationId: string = e.detail.id;
-        const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation 
+        const annotationUri: string = StrCast(e.detail.uri).split('#annotations:')[0]; // clean hypothes.is URLs that reference a specific annotation
         const sourceDoc: Doc = await getSourceWebDoc(annotationUri);
 
-        if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
+        if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) {
+            // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
             runInAction(() => {
                 DocumentLinksButton.AnnotationId = annotationId;
                 DocumentLinksButton.AnnotationUri = annotationUri;
                 DocumentLinksButton.StartLink = sourceDoc;
                 DocumentLinksButton.StartLinkView = undefined;
             });
-        } else { // if a link has already been started, complete the link to sourceDoc
+        } else {
+            // if a link has already been started, complete the link to sourceDoc
             runInAction(() => {
                 DocumentLinksButton.AnnotationId = annotationId;
                 DocumentLinksButton.AnnotationUri = annotationUri;
@@ -81,31 +81,41 @@ export namespace Hypothesis {
     export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => {
         // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client
         // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done
-        !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
+        //!DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
 
         var success = false;
         const onSuccess = action(() => {
-            console.log("Edit success!!");
+            console.log('Edit success!!');
             success = true;
             clearTimeout(interval);
-            DocumentLinksButton.invisibleWebDoc = undefined;
-            document.removeEventListener("editSuccess", onSuccess);
+            //DocumentLinksButton.invisibleWebDoc = undefined;
+            document.removeEventListener('editSuccess', onSuccess);
         });
 
         const newHyperlink = `[${title}\n](${url})`;
-        const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful
-            !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", {
-                detail: { newHyperlink: newHyperlink, id: annotationId },
-                bubbles: true
-            })), 300);
-
-        setTimeout(action(() => {
-            if (!success) {
-                clearInterval(interval);
-                DocumentLinksButton.invisibleWebDoc = undefined;
-            }
-        }), 10000); // give up if no success after 10s
-        document.addEventListener("editSuccess", onSuccess);
+        const interval = setInterval(
+            () =>
+                // keep trying to edit until annotations have loaded and editing is successful
+                !success &&
+                document.dispatchEvent(
+                    new CustomEvent<{ newHyperlink: string; id: string }>('addLink', {
+                        detail: { newHyperlink: newHyperlink, id: annotationId },
+                        bubbles: true,
+                    })
+                ),
+            300
+        );
+
+        setTimeout(
+            action(() => {
+                if (!success) {
+                    clearInterval(interval);
+                    //DocumentLinksButton.invisibleWebDoc = undefined;
+                }
+            }),
+            10000
+        ); // give up if no success after 10s
+        document.addEventListener('editSuccess', onSuccess);
     };
 
     /**
@@ -114,33 +124,40 @@ export namespace Hypothesis {
     export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => {
         if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation
 
-        !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
+        //!DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
 
         var success = false;
         const onSuccess = action(() => {
-            console.log("Edit success!");
+            console.log('Edit success!');
             success = true;
             clearTimeout(interval);
-            DocumentLinksButton.invisibleWebDoc = undefined;
-            document.removeEventListener("editSuccess", onSuccess);
+            // DocumentLinksButton.invisibleWebDoc = undefined;
+            document.removeEventListener('editSuccess', onSuccess);
         });
 
         const annotationId = StrCast(linkDoc.annotationId);
         const linkUrl = Doc.globalServerPath(sourceDoc);
-        const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
-            !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
-                detail: { targetUrl: linkUrl, id: annotationId },
-                bubbles: true
-            }));
+        const interval = setInterval(() => {
+            // keep trying to edit until annotations have loaded and editing is successful
+            !success &&
+                document.dispatchEvent(
+                    new CustomEvent<{ targetUrl: string; id: string }>('deleteLink', {
+                        detail: { targetUrl: linkUrl, id: annotationId },
+                        bubbles: true,
+                    })
+                );
         }, 300);
 
-        setTimeout(action(() => {
-            if (!success) {
-                clearInterval(interval);
-                DocumentLinksButton.invisibleWebDoc = undefined;
-            }
-        }), 10000); // give up if no success after 10s
-        document.addEventListener("editSuccess", onSuccess);
+        setTimeout(
+            action(() => {
+                if (!success) {
+                    clearInterval(interval);
+                    //DocumentLinksButton.invisibleWebDoc = undefined;
+                }
+            }),
+            10000
+        ); // give up if no success after 10s
+        document.addEventListener('editSuccess', onSuccess);
     };
 
     /**
@@ -149,17 +166,20 @@ export namespace Hypothesis {
     export const scrollToAnnotation = (annotationId: string, target: Doc) => {
         var success = false;
         const onSuccess = () => {
-            console.log("Scroll success!!");
+            console.log('Scroll success!!');
             document.removeEventListener('scrollSuccess', onSuccess);
             clearInterval(interval);
             success = true;
         };
 
-        const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
-            document.dispatchEvent(new CustomEvent('scrollToAnnotation', {
-                detail: annotationId,
-                bubbles: true
-            }));
+        const interval = setInterval(() => {
+            // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
+            document.dispatchEvent(
+                new CustomEvent('scrollToAnnotation', {
+                    detail: annotationId,
+                    bubbles: true,
+                })
+            );
             const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target);
             const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
             targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false);
@@ -168,4 +188,4 @@ export namespace Hypothesis {
         document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client
         setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
     };
-}
\ No newline at end of file
+}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 4e0b061a6..fe1190e27 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -250,8 +250,8 @@ $resizeHandler: 8px;
     .documentDecorations-lock {
         position: absolute;
         background: black;
-        right: 11;
-        top: 30px;
+        right: 23px;
+        top: 3px;
         color: gray;
         height: 14;
         width: 14;
@@ -260,6 +260,7 @@ $resizeHandler: 8px;
         display: flex;
         align-items: center;
         flex-direction: column;
+        border-radius: 15%;
     }
 
     .documentDecorations-rotationPath {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 06eb6c6d7..8e0686038 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -782,6 +782,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
+        const useLock = bounds.r - bounds.x > 120;
         const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
         const rotation = NumCast(seldocview.rootDoc._rotation);
 
@@ -856,9 +857,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     onContextMenu={e => e.preventDefault()}
                                 />
                             )}
-                            <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
-                                <FontAwesomeIcon size="sm" icon="lock" />
-                            </div>
+                            {!useLock ? null : (
+                                <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
+                                    <FontAwesomeIcon size="sm" icon="lock" />
+                                </div>
+                            )}
 
                             {hideDocumentButtonBar ? null : (
                                 <div
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c19e6831f..5a6caf995 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -139,7 +139,7 @@ export class KeyManager {
                 document.body.focus();
                 break;
             case 'enter': {
-                DocumentDecorations.Instance.onCloseClick(false);
+                //DocumentDecorations.Instance.onCloseClick(false);
                 break;
             }
             case 'delete':
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3e6539c5e..f327f3184 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -3,9 +3,8 @@
 // }
 
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { AssignAllExtensions } from '../../extensions/General/Extensions';
-import { Utils } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs } from '../documents/Documents';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
@@ -14,7 +13,7 @@ import { ReplayMovements } from '../util/ReplayMovements';
 import { TrackMovements } from '../util/TrackMovements';
 import { CollectionView } from './collections/CollectionView';
 import { MainView } from './MainView';
-import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
+import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
 dotenv.config();
 
 AssignAllExtensions();
@@ -45,6 +44,6 @@ AssignAllExtensions();
         new LinkManager();
         new TrackMovements();
         new ReplayMovements();
-        ReactDOM.render(<MainView />, document.getElementById('root'));
+        ReactDOM.createRoot(document.getElementById('root')!).render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index c5ac1cf52..069206126 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -371,12 +371,3 @@ h1,
     width: 200px;
     height: 800px;
 }
-
-.mainVew-invisibleWebRef {
-    position: absolute;
-    left: 50;
-    top: 50;
-    display: block;
-    width: 500px;
-    height: 1000px;
-}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 052846e71..9648a7807 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -7,7 +7,7 @@ import { action, computed, configure, observable, reaction, runInAction } from '
 import { observer } from 'mobx-react';
 import 'normalize.css';
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { ScriptField } from '../../fields/ScriptField';
 import { DocCast, StrCast } from '../../fields/Types';
@@ -948,40 +948,6 @@ export class MainView extends React.Component {
         );
     }
 
-    @computed get invisibleWebBox() {
-        // see note under the makeLink method in HypothesisUtils.ts
-        return !DocumentLinksButton.invisibleWebDoc ? null : (
-            <div className="mainView-invisibleWebRef" ref={DocumentLinksButton.invisibleWebRef}>
-                <WebBox
-                    fieldKey={'data'}
-                    ContainingCollectionView={undefined}
-                    ContainingCollectionDoc={undefined}
-                    Document={DocumentLinksButton.invisibleWebDoc}
-                    dropAction={'move'}
-                    styleProvider={undefined}
-                    isSelected={returnFalse}
-                    select={returnFalse}
-                    setHeight={returnFalse}
-                    rootSelected={returnFalse}
-                    renderDepth={0}
-                    addDocTab={returnFalse}
-                    pinToPres={returnFalse}
-                    ScreenToLocalTransform={Transform.Identity}
-                    bringToFront={returnFalse}
-                    isContentActive={emptyFunction}
-                    whenChildContentsActiveChanged={returnFalse}
-                    focus={returnFalse}
-                    docViewPath={returnEmptyDoclist}
-                    PanelWidth={() => 500}
-                    PanelHeight={() => 800}
-                    docFilters={returnEmptyFilter}
-                    docRangeFilters={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                />
-            </div>
-        );
-    }
-
     render() {
         return (
             <div
@@ -1037,68 +1003,10 @@ export class MainView extends React.Component {
                 <RichTextMenu />
                 <InkTranscription />
                 {this.snapLines}
-                <div className="mainView-webRef" ref={this.makeWebRef} />
                 <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
             </div>
         );
     }
-
-    makeWebRef = (ele: HTMLDivElement) => {
-        reaction(
-            () => DocumentLinksButton.invisibleWebDoc,
-            invisibleDoc => {
-                ReactDOM.unmountComponentAtNode(ele);
-                invisibleDoc &&
-                    ReactDOM.render(
-                        <span title="Drag as document" className="invisible-webbox">
-                            <div className="mainView-webRef" ref={DocumentLinksButton.invisibleWebRef}>
-                                <WebBox
-                                    fieldKey={'data'}
-                                    ContainingCollectionView={undefined}
-                                    ContainingCollectionDoc={undefined}
-                                    Document={invisibleDoc}
-                                    dropAction={'move'}
-                                    isSelected={returnFalse}
-                                    docViewPath={returnEmptyDoclist}
-                                    select={returnFalse}
-                                    rootSelected={returnFalse}
-                                    renderDepth={0}
-                                    setHeight={returnFalse}
-                                    styleProvider={undefined}
-                                    addDocTab={returnFalse}
-                                    pinToPres={returnFalse}
-                                    ScreenToLocalTransform={Transform.Identity}
-                                    bringToFront={returnFalse}
-                                    isContentActive={emptyFunction}
-                                    whenChildContentsActiveChanged={returnFalse}
-                                    focus={returnFalse}
-                                    PanelWidth={() => 500}
-                                    PanelHeight={() => 800}
-                                    docFilters={returnEmptyFilter}
-                                    docRangeFilters={returnEmptyFilter}
-                                    searchFilterDocs={returnEmptyDoclist}
-                                />
-                            </div>
-                            ;
-                        </span>,
-                        ele
-                    );
-
-                let success = false;
-                const onSuccess = () => {
-                    success = true;
-                    clearTimeout(interval);
-                    document.removeEventListener('editSuccess', onSuccess);
-                };
-
-                // For some reason, Hypothes.is annotations don't load until a click is registered on the page,
-                // so we keep simulating clicks until annotations have loaded and editing is successful
-                const interval = setInterval(() => !success && simulateMouseClick(ele, 50, 50, 50, 50), 500);
-                setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
-                document.addEventListener('editSuccess', onSuccess);
-            }
-        );
-    };
 }
 
 ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) {
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index d9a989309..ce0e58d90 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -27,6 +27,7 @@ export interface MarqueeAnnotatorProps {
     mainCont: HTMLDivElement;
     docView: DocumentView;
     savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
+    selectionText: () => string;
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
     getPageFromScroll?: (top: number) => number;
@@ -147,7 +148,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
             return marqueeAnno;
         }
 
-        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
+        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
         let minX = Number.MAX_VALUE;
         let maxX = -Number.MAX_VALUE;
         let minY = Number.MAX_VALUE;
@@ -271,7 +272,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                     width: `${this._width}px`,
                     height: `${this._height}px`,
                     border: `${this._width === 0 ? '' : '2px dashed black'}`,
-                }}/>
+                }}
+            />
         );
     }
 }
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 12f41394d..df1eb72ce 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -15,6 +15,7 @@ import { SearchBox } from './search/SearchBox';
 import './SidebarAnnos.scss';
 import { StyleProp } from './StyleProvider';
 import React = require('react');
+import { RichTextField } from '../../fields/RichTextField';
 
 interface ExtraProps {
     fieldKey: string;
@@ -72,6 +73,27 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         FormattedTextBox.DontSelectInitialText = true;
         this.allMetadata.map(tag => (target[tag] = tag));
         DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+
+        Doc.GetProto(target).text = new RichTextField(
+            JSON.stringify({
+                doc: {
+                    type: 'doc',
+                    content: [
+                        {
+                            type: 'paragraph',
+                            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                            content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }],
+                        },
+                        {
+                            type: 'paragraph',
+                            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                        },
+                    ],
+                },
+                selection: { type: 'text', anchor: 4, head: 4 },
+            }),
+            ''
+        );
         this.addDocument(target);
         this._stackRef.current?.focusDocument(target, {});
         return target;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 869570a17..5abf4bde2 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -288,11 +288,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             return undefined;
         case StyleProp.Decorations:
             if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
-                return doc &&
-                    isBackground() &&
-                    !Doc.IsSystem(doc) &&
-                    (props?.renderDepth || 0) > 0 &&
-                    ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? (
+                return doc && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
                     <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
                         <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" />
                     </div>
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index ac3541f2c..78e44dfa2 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -89,6 +89,10 @@
     position: relative;
 }
 
+.lm_stack {
+    position: relative;
+}
+
 .lm_drag_tab {
     padding: 0;
     width: 15px !important;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e9b41de25..92319d080 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,6 +1,6 @@
 import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import * as GoldenLayout from '../../../client/goldenLayout';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
@@ -318,7 +318,7 @@ export class CollectionDockingView extends CollectionSubView() {
         }
     };
 
-    componentDidMount: () => void = () => {
+    componentDidMount: () => void = async () => {
         if (this._containerRef.current) {
             this._lightboxReactionDisposer = reaction(
                 () => LightboxView.LightboxDoc,
@@ -335,8 +335,11 @@ export class CollectionDockingView extends CollectionSubView() {
                     this._ignoreStateChange = '';
                 }
             );
-            setTimeout(this.setupGoldenLayout);
-            //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+            reaction(
+                () => this.props.PanelWidth(),
+                width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows
+                { fireImmediately: true }
+            );
         }
     };
 
@@ -460,7 +463,7 @@ export class CollectionDockingView extends CollectionSubView() {
     };
 
     tabDestroyed = (tab: any) => {
-        if (![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
+        if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
             Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
             Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
         }
@@ -470,8 +473,8 @@ export class CollectionDockingView extends CollectionSubView() {
             Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc);
             this.tabMap.delete(tab);
             tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
-            tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
-            setTimeout(this.stateChanged);
+            //tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
+            this.stateChanged();
         }
     };
     tabCreated = (tab: any) => {
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index cde5132a3..31ed5a83b 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -4,7 +4,7 @@ import { Tooltip } from '@material-ui/core';
 import { clamp } from 'lodash';
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -143,8 +143,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
 
                 const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />;
                 const closeIcon = <FontAwesomeIcon icon={'eye'} />;
-                ReactDOM.render(docIcon, iconWrap);
-                ReactDOM.render(closeIcon, closeWrap);
+                ReactDOM.createRoot(iconWrap).render(docIcon);
+                ReactDOM.createRoot(closeWrap).render(closeIcon);
                 tab.reactComponents = [iconWrap, closeWrap];
                 tab.element[0].prepend(iconWrap);
                 tab._disposers.layerDisposer = reaction(
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 1d455ad08..627487a9e 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -40,9 +40,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
     @observable public static AnnotationUri: string | undefined;
     @observable public static LinkEditorDocView: DocumentView | undefined;
 
-    @observable public static invisibleWebDoc: Opt<Doc>;
-    public static invisibleWebRef = React.createRef<HTMLDivElement>();
-
     @action
     @undoBatch
     onLinkButtonMoved = (e: PointerEvent) => {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index c7001f846..fcb3ccb07 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -74,7 +74,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         if (oldDiv.className === 'pdfBox-ui' || oldDiv.className === 'pdfViewerDash-overlay-inking') {
             newDiv.style.display = 'none';
         }
-        if (newDiv && newDiv.style) newDiv.style.overflow = 'hidden';
+        if (newDiv?.style) newDiv.style.overflow = 'hidden';
         if (oldDiv instanceof HTMLCanvasElement) {
             const canvas = oldDiv;
             const img = document.createElement('img'); // create a Image Element
@@ -398,9 +398,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             </div>
         );
     }
-    sidebarWidth = () =>
-        !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth));
-
+    sidebarWidth = () => {
+        if (!this.SidebarShown) return 0;
+        if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target)
+        const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
+        return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
+    };
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
         funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
@@ -483,7 +486,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         crop={this.crop}
                     />
                 </div>
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}>
+                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
                         {...this.props}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index db493934a..5ce6a0eb1 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -836,9 +836,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             }
         }
     });
-    sidebarWidth = () =>
-        !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth));
-
+    sidebarWidth = () => {
+        if (!this.SidebarShown) return 0;
+        if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target)
+        const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
+        return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
+    };
     @computed get content() {
         const interactive =
             !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
@@ -1000,6 +1003,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 docView={this.props.docViewPath().lastElement()}
                                 finishMarquee={this.finishMarquee}
                                 savedAnnotations={this.savedAnnotationsCreator}
+                                selectionText={returnEmptyString}
                                 annotationLayer={this._annotationLayer.current}
                                 mainCont={this._mainCont.current}
                             />{' '}
@@ -1015,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     }}
                     onPointerDown={e => this.sidebarBtnDown(e, false)}
                 />
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}>
+                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
                         {...this.props}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 2b83e9da8..42d20ba99 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,7 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import Color from 'color';
 import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -12,7 +11,7 @@ import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { WebField } from '../../../../fields/URLField';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, Utils } from '../../../../Utils';
+import { aggregateBounds, StopEvent, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
@@ -382,12 +381,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         //     style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
         //     <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
         // </div>;
-        setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
+        //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
         return (
             <div
                 className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`}
                 style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
-                onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))}
+                onClick={action(e => {
+                    this.colorPickerClosed = !this.colorPickerClosed;
+                    e.stopPropagation();
+                })}
                 onPointerDown={e => e.stopPropagation()}>
                 {this.Icon(color)}
                 <div className="colorButton-color" style={{ backgroundColor: curColor }} />
@@ -395,7 +397,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 {/* {dropdownCaret} */}
                 {this.colorPickerClosed ? null : (
                     <div>
-                        <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
+                        <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
                             {this.colorPicker(curColor)}
                         </div>
                         <div
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 40dd6fbc7..85523c176 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -1,5 +1,5 @@
 import { TextSelection } from 'prosemirror-state';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc } from '../../../../fields/Doc';
 import { DocServer } from '../../../DocServer';
 import React = require('react');
@@ -9,6 +9,7 @@ import React = require('react');
 // the comment can be toggled on/off with the '<-' text anchor.
 export class DashDocCommentView {
     dom: HTMLDivElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any) {
         this.dom = document.createElement('div');
@@ -30,12 +31,13 @@ export class DashDocCommentView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this.dom);
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
         (this as any).dom = this.dom;
     }
 
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
 
     selectNode() {}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 5f576be41..63ee7d1f3 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -1,7 +1,7 @@
 import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { NodeSelection } from 'prosemirror-state';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnFalse, Utils } from '../../../../Utils';
@@ -15,6 +15,7 @@ import React = require('react');
 
 export class DashDocView {
     dom: HTMLSpanElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this.dom = document.createElement('span');
@@ -38,14 +39,13 @@ export class DashDocView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(
-            <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />,
-            this.dom
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(
+            <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
         );
-        (this as any).dom = this.dom;
     }
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
     selectNode() {}
 }
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 35d919f38..f088b39d1 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,6 +1,6 @@
 import { action, computed, IReactionDisposer, observable } from 'mobx';
 import { observer } from 'mobx-react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
@@ -19,6 +19,7 @@ import { CollectionViewType } from '../../../documents/DocumentTypes';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey);
@@ -43,11 +44,11 @@ export class DashFieldView {
             e.stopPropagation();
         };
 
-        setTimeout(() => ReactDOM.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} tbox={tbox} />, this.dom));
-        (this as any).dom = this.dom;
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />);
     }
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        //this.root.unmount();
     }
     selectNode() {}
 }
@@ -59,6 +60,7 @@ interface IDashFieldViewInternal {
     tbox: FormattedTextBox;
     width: number;
     height: number;
+    editable: boolean;
 }
 
 @observer
@@ -116,7 +118,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
                 return (
                     <span
                         className="dashFieldView-fieldSpan"
-                        contentEditable={true}
+                        contentEditable={this.props.editable}
                         style={{ display: strVal.length < 2 ? 'inline-block' : undefined }}
                         suppressContentEditableWarning={true}
                         defaultValue={strVal}
@@ -161,6 +163,41 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
         if (nodeText) {
             const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText;
 
+            // const json = {
+            //     doc: {
+            //         type: 'doc',
+            //         content: [
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: '7e02a420-8add-49b0-ad20-54680567575f', hideKey: true, editable: false }, marks: [{ type: 'strong' }] }],
+            //             },
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334077 } }] }],
+            //             },
+            //         ],
+            //     },
+            // };
+
+            // const json = {
+            //     doc: {
+            //         type: 'doc',
+            //         content: [
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'dashField', attrs: { fieldKey: 'hello', docid: '', hideKey: true, editable: true }, marks: [{ type: 'strong' }, { type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334006 } }] }],
+            //             },
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334088 } }] }],
+            //             },
+            //         ],
+            //     },
+            // };
             // look for a document whose id === the fieldKey being displayed.  If there's a match, then that document
             // holds the different enumerated values for the field in the titles of its collected documents.
             // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
@@ -266,14 +303,12 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
         document.addEventListener('pointerdown', hideMenu, true);
     };
     render() {
-        const buttons = [
+        return this.getElement([
             <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
                 <button className="antimodeMenu-button" onPointerDown={this.showFields}>
                     <FontAwesomeIcon icon="eye" size="lg" />
                 </button>
             </Tooltip>,
-        ];
-
-        return this.getElement(buttons);
+        ]);
     }
 }
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 4895dcdc5..0fd2a7808 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -2,7 +2,7 @@ import EquationEditor from 'equation-editor-react';
 import { IReactionDisposer } from 'mobx';
 import { observer } from 'mobx-react';
 import { TextSelection } from 'prosemirror-state';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
 import './DashFieldView.scss';
@@ -11,7 +11,7 @@ import React = require('react');
 
 export class EquationView {
     dom: HTMLDivElement; // container for label and value
-
+    root: any;
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
@@ -22,13 +22,13 @@ export class EquationView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />, this.dom);
-        (this as any).dom = this.dom;
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />);
     }
     _editor: EquationEditor | undefined;
     setEditor = (editor?: EquationEditor) => (this._editor = editor);
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
     setSelection() {
         this._editor?.mathField.focus();
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 096f9a92c..0e48dd93a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1132,6 +1132,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         );
         quickScroll = undefined;
         this.tryUpdateScrollHeight();
+        setTimeout(this.tryUpdateScrollHeight, 250);
     }
 
     pushToGoogleDoc = async () => {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 47833dd43..dc5d8ada8 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -248,8 +248,7 @@ export class RichTextRules {
             // [[fieldKey:Doc]] => show field of doc
             new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
                 const fieldKey = match[1];
-                const rawdocid = match[3];
-                const docid = rawdocid ? normalizeEmail(!rawdocid.includes('@') ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1)) : undefined;
+                const docid = match[3]?.replace(':', '');
                 const value = match[2]?.substring(1);
                 if (!fieldKey) {
                     if (docid) {
@@ -259,7 +258,7 @@ export class RichTextRules {
                             if (rstate) {
                                 this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
                             }
-                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ''), _width: 500, _height: 500 }, docid);
+                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
                             DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined);
 
                             const fstate = this.TextBox.EditorView?.state;
@@ -275,7 +274,7 @@ export class RichTextRules {
                     const num = value.match(/^[0-9.]$/);
                     this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
                 }
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
+                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: true });
                 return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
             }),
 
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 01acc3de9..1fe6d822b 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -1,6 +1,6 @@
 import { TextSelection } from 'prosemirror-state';
 import { Fragment, Node, Slice } from 'prosemirror-model';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import React = require('react');
 
 // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked.
@@ -9,6 +9,7 @@ import React = require('react');
 // method instead of changing prosemirror's text when the expand/elide buttons are clicked.
 export class SummaryView {
     dom: HTMLSpanElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any) {
         const self = this;
@@ -35,13 +36,13 @@ export class SummaryView {
             return js.apply(this, arguments);
         };
 
-        ReactDOM.render(<SummaryViewInternal />, this.dom);
-        (this as any).dom = this.dom;
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<SummaryViewInternal />);
     }
 
     className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed');
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
     selectNode() {}
 
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 66d747bf7..aa2475dca 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -263,6 +263,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             fieldKey: { default: '' },
             docid: { default: '' },
             hideKey: { default: false },
+            editable: { default: true },
         },
         group: 'inline',
         draggable: false,
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 822af6a68..470aa3eb1 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -27,14 +27,14 @@
         opacity: unset;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
 
-        span {
-            padding-right: 5px;
-            padding-bottom: 4px;
-        }
+        // span {
+        //     padding-right: 5px;
+        //     padding-bottom: 4px;
+        // }
     }
 
     .textLayer ::selection {
-        background: #ACCEF7;
+        background: #accef7;
     }
 
     // should match the backgroundColor in createAnnotation()
@@ -111,4 +111,4 @@
 
 .pdfViewerDash-interactive {
     pointer-events: all;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5c10c7cef..abc7336bd 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -79,6 +79,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _initialScroll: Opt<number>;
     private _forcedScroll = true;
 
+    selectionText = () => this._selectionText;
+
     @observable isAnnotating = false;
     // key where data is stored
     @computed get allAnnotations() {
@@ -358,7 +360,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
             MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
             this._marqueeing = [e.clientX, e.clientY];
             this.isAnnotating = true;
-            if (e.target && ((e.target as any).className.includes('endOfContent') || (e.target as any).parentElement.className !== 'textLayer')) {
+            const target = e.target as any;
+            if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
                 this._textSelecting = false;
                 document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called
             } else {
@@ -574,6 +577,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                             docView={this.props.docViewPath().lastElement()}
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
+                            selectionText={this.selectionText}
                             annotationLayer={this._annotationLayer.current}
                             mainCont={this._mainCont.current}
                             anchorMenuCrop={this._textSelecting ? undefined : this.crop}
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 1b12e208c..b8081648f 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { observer } from 'mobx-react';
 import { observable, computed } from 'mobx';
 import { CompileScript } from '../client/util/Scripting';
@@ -11,39 +11,40 @@ import { resolvedPorts } from '../client/util/CurrentUserUtils';
 
 @observer
 class Repl extends React.Component {
-    @observable text: string = "";
+    @observable text: string = '';
 
-    @observable executedCommands: { command: string, result: any }[] = [];
+    @observable executedCommands: { command: string; result: any }[] = [];
 
     onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
         this.text = e.target.value;
-    }
+    };
 
     onKeyDown = (e: React.KeyboardEvent) => {
-        if (!e.ctrlKey && e.key === "Enter") {
+        if (!e.ctrlKey && e.key === 'Enter') {
             e.preventDefault();
             const script = CompileScript(this.text, {
-                addReturn: true, typecheck: false,
-                params: { makeInterface: "any" }
+                addReturn: true,
+                typecheck: false,
+                params: { makeInterface: 'any' },
             });
             if (!script.compiled) {
-                this.executedCommands.push({ command: this.text, result: "Compile Error" });
+                this.executedCommands.push({ command: this.text, result: 'Compile Error' });
             } else {
                 const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e }));
                 result.success && this.executedCommands.push({ command: this.text, result: result.result });
             }
-            this.text = "";
+            this.text = '';
         }
-    }
+    };
 
     @computed
     get commands() {
         return this.executedCommands.map(command => {
             return (
-                <div style={{ marginTop: "5px" }}>
+                <div style={{ marginTop: '5px' }}>
                     <p>{command.command}</p>
                     {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
-                    <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}</pre>
+                    <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}</pre>
                 </div>
             );
         });
@@ -52,16 +53,14 @@ class Repl extends React.Component {
     render() {
         return (
             <div>
-                <div style={{ verticalAlign: "bottom" }}>
-                    {this.commands}
-                </div>
-                <textarea style={{ width: "100%", position: "absolute", bottom: "0px" }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} />
+                <div style={{ verticalAlign: 'bottom' }}>{this.commands}</div>
+                <textarea style={{ width: '100%', position: 'absolute', bottom: '0px' }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} />
             </div>
         );
     }
 }
 
 (async function () {
-    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "repl");
-    ReactDOM.render(<Repl />, document.getElementById("root"));
-})();
\ No newline at end of file
+    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, 'repl');
+    ReactDOM.createRoot(document.getElementById('root')!).render(<Repl />);
+})();
-- 
cgit v1.2.3-70-g09d2


From dcc1c3cb204f5ffe506860daf772f2e978371738 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 2 Nov 2022 09:57:16 -0400
Subject: made decorations semi transparent until hover to make ui less noisy

---
 src/client/views/DocumentDecorations.scss | 31 +++++++++++++------
 src/client/views/DocumentDecorations.tsx  | 51 ++++++++++++++++---------------
 src/client/views/nodes/VideoBox.tsx       |  3 +-
 3 files changed, 51 insertions(+), 34 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index fe1190e27..b41962c73 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -25,6 +25,10 @@ $resizeHandler: 8px;
         align-items: center;
         text-align: center;
         font-size: 30px;
+        opacity: 0.1;
+        &:hover {
+            opacity: 1;
+        }
     }
     .documentDecorations-rotationCenter {
         position: absolute;
@@ -72,10 +76,12 @@ $resizeHandler: 8px;
             height: 20px;
             min-width: 20px;
             border-radius: 100%;
+            opacity: 0.5;
             cursor: pointer;
 
             &:hover {
                 color: #02600d;
+                opacity: 1;
             }
         }
 
@@ -93,10 +99,12 @@ $resizeHandler: 8px;
             height: 20px;
             min-width: 20px;
             border-radius: 100%;
+            opacity: 0.5;
             cursor: pointer;
 
             &:hover {
                 color: #a94442;
+                opacity: 1;
             }
 
             > svg {
@@ -120,10 +128,12 @@ $resizeHandler: 8px;
             height: 20px;
             min-width: 20px;
             border-radius: 100%;
+            opacity: 0.5;
             cursor: pointer;
 
             &:hover {
                 color: #a94442;
+                opacity: 1;
             }
 
             > svg {
@@ -144,6 +154,10 @@ $resizeHandler: 8px;
             border-radius: 8px;
             outline: none;
             border: none;
+            opacity: 0.3;
+            &:hover {
+                opacity: 1;
+            }
 
             .documentDecorations-titleSpan,
             .documentDecorations-titleSpan-Dark {
@@ -206,7 +220,7 @@ $resizeHandler: 8px;
     .documentDecorations-rightResizer {
         pointer-events: auto;
         background: $medium-gray;
-        opacity: 0.1;
+        opacity: 0.2;
         &:hover {
             opacity: 1;
         }
@@ -248,10 +262,8 @@ $resizeHandler: 8px;
     }
 
     .documentDecorations-lock {
-        position: absolute;
+        position: relative;
         background: black;
-        right: 23px;
-        top: 3px;
         color: gray;
         height: 14;
         width: 14;
@@ -261,6 +273,7 @@ $resizeHandler: 8px;
         align-items: center;
         flex-direction: column;
         border-radius: 15%;
+        cursor: default;
     }
 
     .documentDecorations-rotationPath {
@@ -277,7 +290,6 @@ $resizeHandler: 8px;
     .documentDecorations-bottomRightResizer {
         cursor: nwse-resize;
         background: unset;
-        opacity: 1;
         transform: scale(2);
     }
 
@@ -317,7 +329,6 @@ $resizeHandler: 8px;
     .documentDecorations-bottomRightResizer {
         cursor: nwse-resize;
         background: unset;
-        opacity: 1;
     }
 
     .documentDecorations-topLeftResizer {
@@ -333,7 +344,6 @@ $resizeHandler: 8px;
     .documentDecorations-topLeftResizer:hover,
     .documentDecorations-bottomRightResizer:hover {
         opacity: 1;
-        background: black;
     }
 
     .documentDecorations-bottomRightResizer {
@@ -344,7 +354,6 @@ $resizeHandler: 8px;
     .documentDecorations-bottomLeftResizer {
         cursor: nesw-resize;
         background: unset;
-        opacity: 1;
         transform: scale(2);
     }
 
@@ -360,7 +369,6 @@ $resizeHandler: 8px;
 
     .documentDecorations-topRightResizer:hover,
     .documentDecorations-bottomLeftResizer:hover {
-        background: black;
         opacity: 1;
     }
 
@@ -414,6 +422,11 @@ $resizeHandler: 8px;
     gap: 5px;
     top: 4px;
     background: $light-gray;
+    opacity: 0.2;
+    pointer-events: all;
+    &:hover {
+        opacity: 1;
+    }
 }
 
 .linkButtonWrapper {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8e0686038..47347284c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -755,24 +755,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         );
 
         const colorScheme = StrCast(Doc.ActiveDashboard?.colorScheme);
-        const titleArea = this._editingTitle ? (
-            <input
-                ref={this._keyinput}
-                className={`documentDecorations-title${colorScheme}`}
-                type="text"
-                name="dynbox"
-                autoComplete="on"
-                value={hideTitle ? '' : this._accumulatedTitle}
-                onBlur={e => !hideTitle && this.titleBlur()}
-                onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
-                onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
-                onPointerDown={e => e.stopPropagation()}
-            />
-        ) : (
-            <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}>
-                <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span>
-            </div>
-        );
 
         const leftBounds = this.props.boundsLeft;
         const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop;
@@ -782,7 +764,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
-        const useLock = bounds.r - bounds.x > 120;
+        const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView;
         const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
         const rotation = NumCast(seldocview.rootDoc._rotation);
 
@@ -795,6 +777,32 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
         const radiusHandle = (borderRadius / docMax) * maxDist;
         const radiusHandleLocation = Math.min(radiusHandle, maxDist);
+
+        const titleArea = this._editingTitle ? (
+            <input
+                ref={this._keyinput}
+                className={`documentDecorations-title${colorScheme}`}
+                type="text"
+                name="dynbox"
+                autoComplete="on"
+                value={hideTitle ? '' : this._accumulatedTitle}
+                onBlur={e => !hideTitle && this.titleBlur()}
+                onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
+                onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
+                onPointerDown={e => e.stopPropagation()}
+            />
+        ) : (
+            <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}>
+                <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span>
+                {!useLock ? null : (
+                    <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top">
+                        <div className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
+                            <FontAwesomeIcon size="sm" icon="lock" />
+                        </div>
+                    </Tooltip>
+                )}
+            </div>
+        );
         return (
             <div className={`documentDecorations${colorScheme}`}>
                 <div
@@ -857,11 +865,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     onContextMenu={e => e.preventDefault()}
                                 />
                             )}
-                            {!useLock ? null : (
-                                <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
-                                    <FontAwesomeIcon size="sm" icon="lock" />
-                                </div>
-                            )}
 
                             {hideDocumentButtonBar ? null : (
                                 <div
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fb47bfc07..70ac84fa4 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -9,7 +9,7 @@ import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
@@ -1107,6 +1107,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             addDocument={this.addDocWithTimecode}
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
+                            selectionText={returnEmptyString}
                             annotationLayer={this._annotationLayer.current}
                             mainCont={this._mainCont.current}
                             anchorMenuCrop={this.crop}
-- 
cgit v1.2.3-70-g09d2


From 31a51e9dda07e48c88166bffbc8f1ad7166cd624 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 4 Nov 2022 14:34:46 -0400
Subject: more fixes to sidebar annotations of pdfs to improve link following.

---
 src/client/util/LinkFollower.ts                         | 17 +++++++++++++++--
 src/client/views/DocumentDecorations.tsx                |  2 ++
 src/client/views/SidebarAnnos.tsx                       |  3 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx       |  4 +++-
 .../views/nodes/formattedText/DashDocCommentView.tsx    |  9 +++++----
 src/client/views/nodes/formattedText/DashFieldView.tsx  |  3 ---
 .../views/nodes/formattedText/FormattedTextBox.tsx      |  2 +-
 src/client/views/nodes/formattedText/marks_rts.ts       |  2 +-
 8 files changed, 29 insertions(+), 13 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index f75ac24f5..ea0531fa2 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,9 +1,10 @@
-import { action, observable, observe } from 'mobx';
+import { action, observable, observe, runInAction } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { ProxyField } from '../../fields/Proxy';
 import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
 import { DocumentManager } from './DocumentManager';
@@ -59,7 +60,19 @@ export class LinkFollower {
                 docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
             }
         };
-        LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
+        runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
+        LinkFollower.traverseLink(
+            linkDoc,
+            sourceDoc,
+            createViewFunc,
+            BoolCast(sourceDoc.followLinkZoom, zoom),
+            docViewProps.ContainingCollectionDoc,
+            action(() => {
+                batch.end();
+                DocumentDecorations.Instance.overrideBounds = false;
+            }),
+            altKey ? true : undefined
+        );
     };
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 47347284c..cec03c991 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -70,8 +70,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         );
     }
 
+    @observable overrideBounds = false;
     @computed
     get Bounds() {
+        if (this.overrideBounds) return { x: 0, y: 0, r: 0, b: 0 };
         const views = SelectionManager.Views();
         return views
             .filter(dv => dv.props.renderDepth > 0)
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index c285b2e34..ec68a6b36 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -98,9 +98,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                             {
                                 type: 'linkAnchor',
                                 attrs: {
-                                    allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', noPreview: true, anchorId: `${target[Id]}` }],
+                                    allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', anchorId: `${target[Id]}` }],
                                     location: 'add:right',
                                     title: 'Anchored Selection',
+                                    noPreview: true,
                                     docref: false,
                                 },
                             },
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2f246e74f..ac3777aa6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1229,6 +1229,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
         const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
         const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
+        const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top));
+        const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0;
         if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) {
             return {
                 panX: (bounds.left + bounds.right) / 2,
@@ -1238,7 +1240,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         return {
             panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
-            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot),
+            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
         };
     };
 
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 85523c176..4bff57842 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -36,11 +36,12 @@ export class DashDocCommentView {
         (this as any).dom = this.dom;
     }
 
-    destroy() {
-        // ReactDOM.unmountComponentAtNode(this.dom);
+    deselectNode() {
+        this.dom.classList.remove('ProseMirror-selectednode');
+    }
+    selectNode() {
+        this.dom.classList.add('ProseMirror-selectednode');
     }
-
-    selectNode() {}
 }
 
 interface IDashDocCommentViewInternal {
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index fbcf25215..afae59733 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -17,7 +17,6 @@ import { Tooltip } from '@material-ui/core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { NodeSelection } from 'prosemirror-state';
-import { FormattedTextBoxComment } from './FormattedTextBoxComment';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -29,7 +28,6 @@ export class DashFieldView {
         this.dom.style.height = node.attrs.height;
         this.dom.style.position = 'relative';
         this.dom.style.display = 'inline-block';
-        //this.dom.contentEditable = 'true';
         this.dom.onkeypress = function (e: any) {
             e.stopPropagation();
         };
@@ -56,7 +54,6 @@ export class DashFieldView {
     }
     selectNode() {
         this.dom.classList.add('ProseMirror-selectednode');
-        console.log('SELECT');
     }
 }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4378c10f7..63435eea8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1495,7 +1495,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
-            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.noPreview);
+            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview);
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 4206a1006..97549830c 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -113,7 +113,7 @@ export const marks: { [index: string]: MarkSpec } = {
                       ['br'],
                   ]
                 : //node.attrs.allLinks.length === 1 ?
-                  ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, noPreview: node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0];
+                  ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0];
             // ["div", { class: "prosemirror-anchor" },
             //     ["span", { class: "prosemirror-linkBtn" },
             //         ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0],
-- 
cgit v1.2.3-70-g09d2


From bbc993dd70d105c3de078208763e6415eedb1ff7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 11:18:46 -0500
Subject: partial fix for previewing multiple links to different sidebar notes.
  fixed opening sidebar to create an annotation the first time.   fixed
 honoring dashFieldView's non-editable flag.  moved isLinkButton setter to
 expert and make isLinkButton default for Buttons.  fixed following Links with
 Zoom to reset view.  fixed view for editing onClick for documents

---
 src/client/documents/Documents.ts                         |  2 +-
 src/client/util/CurrentUserUtils.ts                       |  2 +-
 src/client/util/LinkFollower.ts                           |  5 +++--
 src/client/views/DocumentButtonBar.tsx                    |  4 ++--
 src/client/views/DocumentDecorations.tsx                  |  2 +-
 src/client/views/nodes/DocumentView.tsx                   |  2 +-
 src/client/views/nodes/LinkDocPreview.tsx                 | 12 +++++++-----
 src/client/views/nodes/formattedText/DashFieldView.tsx    |  3 +++
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 13 ++++++++-----
 src/fields/Proxy.ts                                       |  2 +-
 10 files changed, 28 insertions(+), 19 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d789d4ee3..f1b8c3034 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1607,7 +1607,7 @@ export namespace DocUtils {
         const iconViews = DocListCast(Cast(Doc.UserDoc()['template-icons'], Doc, null)?.data);
         const templBtns = DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data);
         const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data);
         const allTemplates = iconViews
             .concat(templBtns)
             .concat(noteTypes)
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f2d851a40..d13209c4f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -267,7 +267,7 @@ export class CurrentUserUtils {
             {key: "Map",        creator: opts => Docs.Create.MapDocument([], opts),         opts: { _width: 800, _height: 600, _showSidebar: true,  }},
             {key: "Screengrab", creator: Docs.Create.ScreenshotDocument,                    opts: { _width: 400, _height: 200 }},
             {key: "WebCam",     creator: opts => Docs.Create.WebCamDocument("", opts),      opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
-            {key: "Button",     creator: Docs.Create.ButtonDocument,                        opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }},
+            {key: "Button",     creator: Docs.Create.ButtonDocument,                        opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             // {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument(opts),         opts: { _width: 300, _height: 300 }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index ea0531fa2..43b2caf88 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -48,9 +48,10 @@ export class LinkFollower {
                                 },
                             });
                         } else {
-                            res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
+                            finished?.();
+                            res(where !== 'inPlace' || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
                         }
-                    });
+                    }, 100);
                 });
 
             if (!sourceDoc.followLinkZoom) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index ce77b7446..3f12c7c9d 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -356,7 +356,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}>
                 <div
                     className="documentButtonBar-icon"
-                    style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
+                    style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }}
                     onClick={undoBatch(
                         action(e => {
                             this._isRecording = true;
@@ -463,7 +463,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         {this.metadataButton}
                     </div> */
                 }
-                {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
+                {Doc.noviceMode || !SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : (
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index cec03c991..3fac137fe 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -767,7 +767,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
         const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView;
-        const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
+        const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
         const rotation = NumCast(seldocview.rootDoc._rotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4ee871ba2..973363f78 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -618,7 +618,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         clickFunc();
                     }, 350);
                 } else clickFunc();
-            } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+            } else if (this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 SelectionManager.DeselectAll();
                 this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
             } else {
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 0e66214d1..a47577701 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -41,6 +41,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
     _linkDocRef = React.createRef<HTMLDivElement>();
     @observable public static LinkInfo: Opt<LinkDocPreviewProps>;
     @observable _targetDoc: Opt<Doc>;
+    @observable _markerTargetDoc: Opt<Doc>;
     @observable _linkDoc: Opt<Doc>;
     @observable _linkSrc: Opt<Doc>;
     @observable _toolTipText = '';
@@ -57,9 +58,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         }
         if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) {
             // want to show annotation context document if annotation is not text
-            linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._targetDoc = anno)));
+            linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno)));
         } else {
-            this._targetDoc = linkTarget;
+            this._markerTargetDoc = this._targetDoc = linkTarget;
         }
         this._toolTipText = '';
     }
@@ -105,10 +106,11 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                     // automatic links specify the target in the link info, not the source
                                     const linkTarget = anchor;
                                     this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
-                                    this._targetDoc = linkTarget;
+                                    this._markerTargetDoc = this._targetDoc = linkTarget;
                                 } else {
                                     this._linkSrc = anchor;
                                     const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+                                    this._markerTargetDoc = linkTarget;
                                     this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                                 }
                                 this._toolTipText = '';
@@ -175,10 +177,10 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225));
     };
     @computed get previewHeader() {
-        return !this._linkDoc || !this._targetDoc || !this._linkSrc ? null : (
+        return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
             <div className="linkDocPreview-info">
                 <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}>
-                    {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)}
+                    {StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)}
                     <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
                 </div>
                 <div className="linkDocPreview-buttonBar">
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 39b38add2..41b92a3c7 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -171,6 +171,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
             }
             e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
         }
+        if (!this.props.editable) {
+            e.preventDefault();
+        }
         e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fc8d2b7dd..2984feba5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -231,7 +231,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
 
-    getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection');
+    getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', false);
 
     @action
     setupAnchorMenu = () => {
@@ -239,7 +239,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
             !this.layoutDoc.showSidebar && this.toggleSidebar();
-            this._sidebarRef.current?.anchorMenuClick(this.getAnchor());
+            setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
         };
         AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
             !this.layoutDoc.showSidebar && this.toggleSidebar();
@@ -891,9 +891,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     // TODO: nda -- Look at how link anchors are added
-    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) {
+    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean) {
         const state = this._editorView?.state;
         if (state) {
+            let selectedText = '';
             const sel = state.selection;
             const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
             let tr = state.tr.addMark(sel.from, sel.to, splitter);
@@ -905,13 +906,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                         const allAnchors = [{ href, title, anchorId: anchor[Id] }];
                         allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? []));
-                        const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location });
+                        const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location, noPreview });
                         tr = tr.addMark(pos, pos + node.nodeSize, link);
+                        selectedText += (node as Node).textContent;
                     }
                 });
                 this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
                 this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
                 this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
+                anchor.text = selectedText;
                 return anchor;
             }
             return anchorDoc ?? this.rootDoc;
@@ -1471,7 +1474,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
-            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview);
+            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index e924ef7a3..72ae13035 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -66,7 +66,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
 
         const cached = DocServer.GetCachedRefField(this.fieldId) as T;
         if (cached !== undefined) {
-            this.cache = cached;
+            setTimeout(action(() => (this.cache = cached)));
         } else if (!this.promise) {
             this.promise = DocServer.GetRefField(this.fieldId).then(
                 action((field: any) => {
-- 
cgit v1.2.3-70-g09d2


From 19f317bd43a7cc8df0daf1c0642011cc8754e14b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 15 Nov 2022 12:49:53 -0500
Subject: made InPlace container tool button apply more settings to be more
 like a template. added followAllLInks flag.  added image anchors to save pan
 zoom.  added follow link button bar option for follow all links.  added
 hideDecorations flag and property

---
 src/client/documents/Documents.ts                  |  5 ++
 src/client/util/LinkFollower.ts                    | 11 ++--
 src/client/views/DocumentButtonBar.scss            | 31 ++++++++++-
 src/client/views/DocumentButtonBar.tsx             | 60 +++++++++++++++-------
 src/client/views/DocumentDecorations.tsx           | 19 +++----
 src/client/views/PropertiesButtons.tsx             | 17 ++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/linking/LinkEditor.tsx            |  1 -
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/ImageBox.tsx                | 52 ++++++++++++++++---
 src/client/views/nodes/trails/PresBox.tsx          |  3 +-
 11 files changed, 154 insertions(+), 50 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e44004f45..8f45802fe 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -257,6 +257,7 @@ export class DocumentOptions {
     lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
     activeFrame?: number; // the active frame of a document in a frame base collection
     appearFrame?: number; // the frame in which the document appears
+    viewTransitionTime?: number; // transition duration for view parameters
     presTransition?: number; //the time taken for the transition TO a document
     presDuration?: number; //the duration of the slide in presentation view
     borderRounding?: string;
@@ -1047,6 +1048,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
         }
 
+        export function ImageanchorDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        }
+
         export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
         }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index c09c9d1c5..c6fc7b372 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,6 +1,7 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types';
+import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
@@ -76,11 +77,11 @@ export class LinkFollower {
         const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
         const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
         const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
-        const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
-        const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
+        const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor2 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0);
+        const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0);
         const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
-        const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
-        const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1);
+        const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
+        const followLinks = sourceDoc.isPushpin || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
         var count = 0;
         const allFinished = () => ++count === followLinks.length && finished?.();
         followLinks.forEach(async linkDoc => {
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 1e93ba5e2..f9c988fdd 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -18,15 +18,44 @@ $linkGap: 3px;
     cursor: pointer;
 }
 
+.documentButtonBar-followTypes,
 .documentButtonBar-pinTypes {
     position: absolute;
-    display: flex;
+    display: none;
     width: 60px;
     top: -14px;
     background: black;
     height: 20px;
     align-items: center;
 }
+.documentButtonBar-followTypes {
+    width: 20px;
+    display: none;
+}
+.documentButtonBar-followIcon {
+    align-items: center;
+    display: flex;
+    height: 100%;
+    &:hover {
+        background-color: lightblue;
+    }
+}
+.documentButtonBar-follow {
+    &:hover {
+        .documentButtonBar-followTypes {
+            display: flex;
+        }
+    }
+}
+.documentButtonBar-pin {
+    color: white;
+    &:hover {
+        .documentButtonBar-pinTypes {
+            display: flex;
+        }
+    }
+}
+
 .documentButtonBar-pinIcon {
     &:hover {
         background-color: lightblue;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 36875290e..794b51cc5 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -5,7 +5,7 @@ import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
-import { Cast, NumCast } from '../../fields/Types';
+import { BoolCast, Cast, NumCast } from '../../fields/Types';
 import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
@@ -209,21 +209,52 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             </Tooltip>
         );
     }
+    @observable subFollow = '';
     @computed
     get followLinkButton() {
         const targetDoc = this.view0?.props.Document;
+        const followBtn = (allDocs: boolean, click: (doc: Doc) => void, isSet: (doc?: Doc) => boolean, icon: IconProp) => {
+            const tooltip = `Follow ${this.subPin}documents`;
+            return !tooltip ? null : (
+                <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
+                    <div className="documentButtonBar-followIcon" style={{ backgroundColor: isSet(targetDoc) ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: isSet(targetDoc) ? Colors.BLACK : Colors.WHITE }}>
+                        <FontAwesomeIcon
+                            className="documentdecorations-icon"
+                            style={{ width: 20 }}
+                            key={icon.toString()}
+                            size="sm"
+                            icon={icon}
+                            onPointerEnter={action(e => (this.subPin = allDocs ? 'All ' : ''))}
+                            onPointerLeave={action(e => (this.subPin = ''))}
+                            onClick={e => {
+                                this.props.views().forEach(dv => click(dv!.rootDoc));
+                                e.stopPropagation();
+                            }}
+                        />
+                    </div>
+                </Tooltip>
+            );
+        };
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{'Set onClick to follow primary link'}</div>}>
+            <Tooltip title={<div className="dash-tooltip">Set onClick to follow primary link</div>}>
                 <div
-                    className="documentButtonBar-icon"
+                    className="documentButtonBar-icon documentButtonBar-follow"
                     style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
                     onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, undefined, false)))}>
+                    <div className="documentButtonBar-followTypes">
+                        {followBtn(
+                            true,
+                            (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks),
+                            (doc?: Doc) => (doc?.followAllLinks ? true : false),
+                            'window-maximize'
+                        )}
+                    </div>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" />
                 </div>
             </Tooltip>
         );
     }
-    @observable expandPin = false;
+
     @observable subPin = '';
     @computed
     get pinButton() {
@@ -264,10 +295,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-tooltip">{`Pin Document ${SelectionManager.Views().length > 1 ? 'multiple documents' : ''} to Trail`}</div>}>
                 <div
-                    className="documentButtonBar-icon"
-                    style={{ color: 'white' }}
-                    onPointerEnter={action(e => (this.expandPin = true))}
-                    onPointerLeave={action(e => (this.expandPin = false))}
+                    className="documentButtonBar-icon documentButtonBar-pin"
                     onClick={e => {
                         const docs = this.props
                             .views()
@@ -276,13 +304,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         TabDocView.PinDoc(docs, { pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
                         e.stopPropagation();
                     }}>
-                    {this.expandPin ? (
-                        <div className="documentButtonBar-pinTypes">
-                            {pinBtn(true, false, 'window-maximize')}
-                            {pinBtn(false, true, 'address-card')}
-                            {pinBtn(true, true, 'id-card')}
-                        </div>
-                    ) : null}
+                    <div className="documentButtonBar-pinTypes">
+                        {pinBtn(true, false, 'window-maximize')}
+                        {pinBtn(false, true, 'address-card')}
+                        {pinBtn(true, true, 'id-card')}
+                    </div>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
                 </div>
             </Tooltip>
@@ -460,9 +486,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                 ) : null}
                 {
                     Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
-                    /*<div className="documentButtonBar-button">
-                        {this.metadataButton}
-                    </div> */
+                    /*<div className="documentButtonBar-button"> {this.metadataButton} </div> */
                 }
                 {Doc.noviceMode || !SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3fac137fe..3efb5fb37 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -708,24 +708,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     }
 
     render() {
-        const { b, c, r, x, y } = this.Bounds;
-        const bounds = { b, c, r, x, y };
+        const { b, r, x, y } = this.Bounds;
+        const bounds = { b, r, x, y };
         const seldocview = SelectionManager.Views().slice(-1)[0];
         if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
             return null;
         }
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
-        const hideResizers = seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
-        const hideTitle = seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
-        const hideDocumentButtonBar = seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
+        const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations;
+        const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
+        const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
+        const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
         // if multiple documents have been opened at the same time, then don't show open button
-        const hideOpenButton =
+        const hideOpenButton =hideDecorations || 
             seldocview.props.hideOpenButton ||
             seldocview.rootDoc.hideOpenButton ||
             SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
-        const hideDeleteButton =
+        const hideDeleteButton =hideDecorations || 
             this._isRounding ||
             this._isRotating ||
             seldocview.props.hideDeleteButton ||
@@ -767,7 +768,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
         const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView;
-        const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
+        const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
         const rotation = NumCast(seldocview.rootDoc._rotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
@@ -837,7 +838,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}>
                                 {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                 {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
-                                {titleArea}
+                                {hideTitle ? null : titleArea}
                                 {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
                             </div>
                             {hideResizers ? null : (
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 5992b3eb9..d27848a90 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -121,15 +121,24 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
-                    containerDoc.noShadow = containerDoc.noHighlighting = containerDoc._isLinkButton = containerDoc._fitContentsToBox = containerDoc._forceActive = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
+                    containerDoc.followAllLinks =
+                        containerDoc.noShadow =
+                        containerDoc.noHighlighting =
+                        containerDoc._isLinkButton =
+                        containerDoc._fitContentsToBox =
+                        containerDoc._forceActive =
+                        containerDoc._isInPlaceContainer =
+                            !containerDoc._isInPlaceContainer;
                     containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? 'inPlace' : undefined;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
                     const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
                     if (menuDoc) {
-                        menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer;
-                        DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
+                        menuDoc.hideDecorations = menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer;
+                        if (!dv.allLinks.find(link => link.anchor1 === menuDoc || link.anchor2 === menuDoc)) {
+                            DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
+                        }
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
-                            menuItem._isLinkButton = true;
+                            menuItem.followAllLinks = menuItem._isLinkButton = true;
                             menuItem._followLinkLocation = 'inPlace';
                         });
                     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e24b116d0..8fe5ad63f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1157,7 +1157,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
         //     SelectionManager.DeselectAll();
         // }
-        if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) {
+        if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined || doc.type === DocumentType.MARKER) {
             this.props.focus(doc, options);
         } else {
             const xfToCollection = options?.docTransform ?? Transform.Identity();
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 2fb8e3d4c..3e8867c50 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -206,7 +206,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @computed
     get editAudioFollow() {
         //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        console.log('AudioFollow:' + this.audioFollow);
         return (
             <div className="linkEditor-zoomFollow">
                 <div className="linkEditor-zoomFollow-label">Play Target Audio:</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7accd49a4..76d6d3532 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -166,7 +166,8 @@ export interface DocumentViewSharedProps {
 // these props are specific to DocuentViews
 export interface DocumentViewProps extends DocumentViewSharedProps {
     // properties specific to DocumentViews but not to FieldView
-    hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
+    hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
+    hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected
     hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
     hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
     hideDocumentButtonBar?: boolean;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d0df41023..d67481b22 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -14,7 +14,7 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
-import { DocUtils } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
@@ -49,21 +49,58 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     private _dropDisposer?: DragManager.DragDropDisposer;
+    private _transitioning: any; // timer for setting view spec properties
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     @observable _curSuffix = '';
     @observable _uploadIcon = uploadIcons.idle;
+    @observable _focusViewScale: number | undefined = 1;
+    @observable _focusPanX: number | undefined = 0;
+    @observable _focusPanY: number | undefined = 0;
+    get viewScale() {
+        return this._focusViewScale || StrCast(this.layoutDoc._viewScale);
+    }
+    get panX() {
+        return this._focusPanX || StrCast(this.layoutDoc._panX);
+    }
+    get panY() {
+        return this._focusPanY || StrCast(this.layoutDoc._panY);
+    }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer?.();
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
     };
-    setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
+
+    @action
+    setViewSpec = (anchor: Doc, preview: boolean) => {
+        if (preview && anchor._viewScale !== undefined) {
+            this._focusViewScale = Cast(anchor._viewScale, 'number', null);
+            this._focusPanX = Cast(anchor._panX, 'number', null);
+            this._focusPanY = Cast(anchor._panX, 'number', null);
+        } else if (anchor._viewScale !== undefined) {
+            const smoothTime = NumCast(anchor.viewTransitionTime);
+            this.layoutDoc.viewTransition = `all ${smoothTime}ms`;
+            this.layoutDoc._panX = NumCast(anchor._panX, NumCast(this.layoutDoc._panY));
+            this.layoutDoc._panY = NumCast(anchor._panY, NumCast(this.layoutDoc._panX));
+            this.layoutDoc._viewScale = NumCast(anchor._viewScale, NumCast(this.layoutDoc._viewScale));
+            if (anchor.type === DocumentType.MARKER) {
+                this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(anchor._annotations));
+            }
+            clearTimeout(this._transitioning);
+            this._transitioning = setTimeout(() => (this.layoutDoc.viewTransition = undefined), smoothTime);
+        }
+    }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
 
     getAnchor = () => {
-        const anchor = this._getAnchor?.(this._savedAnnotations);
-        anchor && this.addDocument(anchor);
-        return anchor ?? this.rootDoc;
+        const zoomedAnchor = () => Docs.Create.ImageanchorDocument({ viewTransitionTime: 1000, _viewScale: NumCast(this.layoutDoc._viewScale), _panX: NumCast(this.layoutDoc._panX), _panY: NumCast(this.layoutDoc._panY) });
+        const anchor = this._getAnchor?.(this._savedAnnotations) ?? (this.layoutDoc.viewScale ? zoomedAnchor() : undefined);
+        if (anchor) {
+            anchor._annotations = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey]));
+            this.addDocument(anchor);
+            return anchor;
+        }
+        return this.rootDoc;
     };
 
     componentDidMount() {
@@ -90,6 +127,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
 
     componentWillUnmount() {
+        clearTimeout(this._transitioning);
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
@@ -283,9 +321,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     setTimeout(
                         action(() => {
                             this._uploadIcon = idle;
-                            if (data) {
-                                dataDoc[this.fieldKey] = data;
-                            }
+                            data && (dataDoc[this.fieldKey] = data);
                         }),
                         2000
                     );
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index b495a9399..8d805c663 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -509,8 +509,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
         if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) {
             const transTime = NumCast(activeItem.presTransition, 500);
-            const presTransitionTime = `all ${transTime}ms`;
-            targetDoc._dataTransition = presTransitionTime;
+            targetDoc._dataTransition = `all ${transTime}ms`;
             targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x));
             targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y));
             targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation));
-- 
cgit v1.2.3-70-g09d2


From c6d1059e24f362a167b9ac24e6f13d1e45361da9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 29 Nov 2022 12:05:29 -0500
Subject: changes to streamline link editing UI (got rid of LinkEditor). 
 cleaned up link (un)highlighting.

---
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/DocumentManager.ts                 |  19 +-
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/util/LinkManager.ts                     |   2 +-
 src/client/util/SelectionManager.ts                |   1 -
 src/client/views/DocumentDecorations.tsx           |  14 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/PropertiesView.scss               |  15 +-
 src/client/views/PropertiesView.tsx                |  94 +++--
 .../CollectionFreeFormLinksView.tsx                |   2 +-
 src/client/views/global/globalCssVariables.scss    |   1 +
 .../views/global/globalCssVariables.scss.d.ts      |   1 +
 src/client/views/linking/LinkEditor.scss           | 334 ---------------
 src/client/views/linking/LinkEditor.tsx            | 454 ---------------------
 src/client/views/linking/LinkMenu.tsx              |  37 +-
 src/client/views/linking/LinkMenuGroup.tsx         |  50 ++-
 src/client/views/linking/LinkMenuItem.tsx          |   4 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  16 +-
 src/client/views/nodes/LinkAnchorBox.tsx           | 110 ++---
 src/fields/Doc.ts                                  |  64 +--
 21 files changed, 194 insertions(+), 1031 deletions(-)
 delete mode 100644 src/client/views/linking/LinkEditor.scss
 delete mode 100644 src/client/views/linking/LinkEditor.tsx

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c5e08eeea..eed839520 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1375,7 +1375,6 @@ export namespace DocUtils {
                     'acl-Public': SharingPermissions.Augment,
                     '_acl-Public': SharingPermissions.Augment,
                     linkDisplay: true,
-                    _hidden: true,
                     _linkAutoMove: true,
                     linkRelationship,
                     _showCaption: 'description',
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 7120eeb88..4f02a8202 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
 import { action, observable, runInAction } from 'mobx';
 import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { Cast } from '../../fields/Types';
+import { Cast, DocCast } from '../../fields/Types';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LightboxView } from '../views/LightboxView';
@@ -36,15 +36,14 @@ export class DocumentManager {
         //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
         if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
             const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1';
-            DocListCast(view.rootDoc.links).forEach(link => {
-                this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
-                    this.LinkedDocumentViews.push({
-                        a: viewAnchorIndex === 'anchor2' ? otherView : view,
-                        b: viewAnchorIndex === 'anchor2' ? view : otherView,
-                        l: link,
-                    })
-                );
-            });
+            const link = view.rootDoc;
+            this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+                this.LinkedDocumentViews.push({
+                    a: viewAnchorIndex === 'anchor2' ? otherView : view,
+                    b: viewAnchorIndex === 'anchor2' ? view : otherView,
+                    l: link,
+                })
+            );
             this.LinkAnchorBoxViews.push(view);
             // this.LinkedDocumentViews.forEach(view => console.log("  LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " +
             //     view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString));
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 5374bf44b..282116f1b 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -67,7 +67,7 @@ export class LinkFollower {
             docViewProps.ContainingCollectionDoc,
             action(() => {
                 batch.end();
-                DocumentDecorations.Instance.overrideBounds = false;
+                Doc.AddUnlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
             }),
             altKey ? true : undefined
         );
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 49cc3218d..01f4df723 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -44,7 +44,7 @@ export class LinkManager {
                         if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
                             Doc.GetProto(a1)[DirectLinksSym].add(link);
                             Doc.GetProto(a2)[DirectLinksSym].add(link);
-                            Doc.GetProto(link)[DirectLinksSym].add(link);
+                            //Doc.GetProto(link)[DirectLinksSym].add(link); // bcz: links are not linked to themself, so this was a hack
                         }
                     })
                 );
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 02d672a65..a3d6f5227 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,7 +4,6 @@ import { Doc, Opt } from '../../fields/Doc';
 import { DocCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocumentView } from '../views/nodes/DocumentView';
-import { LinkManager } from './LinkManager';
 import { ScriptingGlobals } from './ScriptingGlobals';
 
 export namespace SelectionManager {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3efb5fb37..d1f0bf2ac 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,7 +27,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
@@ -253,17 +253,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         if (selectedDocs.length) {
             if (e.ctrlKey) {
                 // open an alias in a new tab with Ctrl Key
-                CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), 'right');
+                CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), OpenWhereMod.right);
             } else if (e.shiftKey) {
                 // open centered in a new workspace with Shift Key
                 const alias = Doc.MakeAlias(selectedDocs[0].props.Document);
                 alias.context = undefined;
                 alias.x = -alias[WidthSym]() / 2;
                 alias.y = -alias[HeightSym]() / 2;
-                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), 'right');
+                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), OpenWhereMod.right);
             } else if (e.altKey) {
                 // open same document in new tab
-                CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, 'right');
+                CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right);
             } else {
                 var openDoc = selectedDocs[0].props.Document;
                 if (openDoc.layoutKey === 'layout_icon') {
@@ -720,13 +720,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
         const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
         // if multiple documents have been opened at the same time, then don't show open button
-        const hideOpenButton =hideDecorations || 
+        const hideOpenButton =
+            hideDecorations ||
             seldocview.props.hideOpenButton ||
             seldocview.rootDoc.hideOpenButton ||
             SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
-        const hideDeleteButton =hideDecorations || 
+        const hideDeleteButton =
+            hideDecorations ||
             this._isRounding ||
             this._isRotating ||
             seldocview.props.hideDeleteButton ||
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 98d0378be..09063901d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -484,7 +484,7 @@ export class MainView extends React.Component {
     }
 
     globalPointerDown = action((e: PointerEvent) => {
-        runInAction(() => (Doc.HighlightBrush.linkFollowEffect = undefined));
+        Doc.linkFollowUnhighlight();
         AudioBox.Enabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets.length) {
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 30806f718..897be9a32 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -4,8 +4,13 @@
     height: 100%;
     width: 250;
     font-family: 'Roboto';
+    font-size: 12px;
     cursor: auto;
 
+    .slider-text {
+        font-size: 8px;
+    }
+
     overflow-x: hidden;
     overflow-y: auto;
 
@@ -865,7 +870,15 @@
     }
 
     .propertiesButton {
-        width: 4rem;
+        width: 2rem;
+        height: 2rem;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        > svg {
+            width: 15px;
+            height: 15px;
+        }
     }
 }
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ad3f62990..e8fd540a8 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -55,7 +55,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     }
 
     @computed get selectedDoc() {
-        return LinkManager.currentLink || SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard;
+        return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard;
     }
     @computed get selectedDocumentView() {
         if (SelectionManager.Views().length) return SelectionManager.Views()[0];
@@ -306,7 +306,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     }
 
     @computed get links() {
-        return !this.selectedDoc ? null : <PropertiesDocBacklinksSelector Document={this.selectedDoc} hideTitle={true} addDocTab={this.props.addDocTab} />;
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc;
+        return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />;
     }
 
     @computed get layoutPreview() {
@@ -1426,11 +1427,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @undoBatch
     animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
         const lanch = this.sourceAnchor;
-        const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+        const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.MEDIUM_BLUE : '';
         return (
             <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                 <div
-                    style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+                    style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '20%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', background: color, color: 'black' }}
                     onClick={() => this.updateEffectDirection(direction)}>
                     {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
                 </div>
@@ -1470,19 +1471,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         }
     };
 
-    toggleProp = (e: React.PointerEvent, prop: string) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc[prop] = !this.selectedDoc[prop]))));
+    toggleLinkProp = (e: React.PointerEvent, prop: string) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop]))));
     };
 
     @computed get destinationAnchor() {
         const ldoc = LinkManager.currentLink;
-        const lanch = LinkManager.currentLinkAnchor;
+        const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
         if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch;
         return ldoc ? DocCast(ldoc.anchor2) : ldoc;
     }
 
     @computed get sourceAnchor() {
-        return LinkManager.currentLinkAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
+
+        return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
     }
 
     toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc) => {
@@ -1545,9 +1548,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         const isNovice = Doc.noviceMode;
         const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
-        const selectedDoc = this.selectedDoc;
         const indent = 30;
-        if (!selectedDoc && !this.isPres) {
+        if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this.props.width }}>
                     <div className="propertiesView-title" style={{ width: this.props.width }}>
@@ -1556,7 +1558,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 </div>
             );
         } else {
-            if (selectedDoc && !this.isPres) {
+            if (this.selectedDoc && !this.isPres) {
                 return (
                     <div
                         className="propertiesView"
@@ -1573,9 +1575,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         {this.contextsSubMenu}
 
                         {this.linksSubMenu}
-                        {!selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(dv.rootDoc.links).includes(LinkManager.currentLink!)) ? null : (
+                        {!this.selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!)) ? null : (
                             <>
-                                <div className="propertiesView-section">
+                                <div className="propertiesView-section" style={{ background: 'darkgray' }}>
                                     <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
                                         <p>Relationship</p>
                                         {this.editRelationship}
@@ -1584,11 +1586,41 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         <p>Description</p>
                                         {this.editDescription}
                                     </div>
+                                    <div className="propertiesView-input inline">
+                                        <p>Show link</p>
+                                        <button
+                                            style={{ background: !LinkManager.currentLink?.linkDisplay ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleLinkProp(e, 'linkDisplay')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
+                                        <p>Auto-move anchors</p>
+                                        <button
+                                            style={{ background: !LinkManager.currentLink?.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleLinkProp(e, 'linkAutoMove')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
+                                        <p>Display arrow</p>
+                                        <button
+                                            style={{ background: !LinkManager.currentLink?.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleLinkProp(e, 'linkDisplayArrow')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
                                 </div>
                                 <div className="propertiesView-section">
                                     <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}>
                                         <p>Follow by</p>
-                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(selectedDoc.followLinkLocation, 'default')}>
+                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkLocation, 'default')}>
                                             <option value="default">Default</option>
                                             <option value="add:left">Opening in new left pane</option>
                                             <option value="add:right">Opening in new right pane</option>
@@ -1598,7 +1630,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <option value="add">Opening in new tab</option>
                                             <option value="replace">Replacing current tab</option>
                                             <option value="inPlace">Opening in place</option>
-                                            {selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+                                            {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
                                         </select>
                                     </div>
                                     <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
@@ -1609,7 +1641,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <option value={effect.toString()}>{effect.toString()}</option>
                                             ))}
                                         </select>
-                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, gridColumn: 3 }}>
+                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
                                             {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
                                             {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
                                             {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
@@ -1650,34 +1682,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
                                         </button>
                                     </div>
-                                    <div className="propertiesView-input inline">
-                                        <p>Show link</p>
-                                        <button style={{ background: !selectedDoc.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} onPointerDown={e => this.toggleProp(e, 'linkDisplay')} onClick={e => e.stopPropagation()} className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
-                                        <p>Auto-move anchor</p>
-                                        <button
-                                            style={{ background: !selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
-                                            onPointerDown={e => this.toggleProp(e, 'linkAutoMove')}
-                                            onClick={e => e.stopPropagation()}
-                                            className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
-                                        <p>Display arrow</p>
-                                        <button
-                                            style={{ background: !selectedDoc.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
-                                            onPointerDown={e => this.toggleProp(e, 'linkDisplayArrow')}
-                                            onClick={e => e.stopPropagation()}
-                                            className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 128px) 50px' }}>
-                                        <p>Zoom % screen</p>
+                                    <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
+                                        <p>Zoom %</p>
                                         <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
                                             <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
                                             <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index b8344dc0c..9e360f557 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -2,10 +2,10 @@ import { computed } from 'mobx';
 import { observer } from 'mobx-react';
 import { Id } from '../../../../fields/FieldSymbols';
 import { DocumentManager } from '../../../util/DocumentManager';
+import { LightboxView } from '../../LightboxView';
 import './CollectionFreeFormLinksView.scss';
 import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView';
 import React = require('react');
-import { LightboxView } from '../../LightboxView';
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> {
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index e68f9abe3..430a36dce 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -84,4 +84,5 @@ $TREE_BULLET_WIDTH: 20px;
     LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
     TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
     INK_MASK_SIZE: $INK_MASK_SIZE;
+    MEDIUM_GRAY: $medium-gray;
 }
diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts
index 76259113c..3375579d6 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -11,6 +11,7 @@ interface IGlobalScss {
     LEFT_MENU_WIDTH: string;
     TREE_BULLET_WIDTH: string;
     INK_MASK_SIZE: number;
+    MEDIUM_GRAY: string;
 }
 declare const globalCssVariables: IGlobalScss;
 
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
deleted file mode 100644
index b0ee4e46d..000000000
--- a/src/client/views/linking/LinkEditor.scss
+++ /dev/null
@@ -1,334 +0,0 @@
-@import '../global/globalCssVariables';
-
-.linkEditor {
-    width: 100%;
-    height: auto;
-    font-size: 13px; // TODO
-    user-select: none;
-    max-width: 280px;
-}
-
-.linkEditor-button-back {
-    //margin-bottom: 6px;
-    border-radius: 10px;
-    width: 18px;
-    height: 18px;
-    padding: 0;
-
-    &:hover {
-        cursor: pointer;
-    }
-}
-
-.linkEditor-info {
-    padding-top: 12px;
-    padding-left: 5px;
-    padding-bottom: 3px;
-    //margin-bottom: 6px;
-    display: flex;
-    justify-content: space-between;
-    color: black;
-
-    .linkEditor-linkedTo {
-        width: calc(100% - 46px);
-        overflow: hidden;
-        position: relative;
-        text-overflow: ellipsis;
-        white-space: pre;
-
-        .linkEditor-downArrow {
-            &:hover {
-                cursor: pointer;
-            }
-        }
-    }
-}
-
-.linkEditor-moreInfo {
-    margin-left: 12px;
-    padding-left: 13px;
-    padding-right: 6.5px;
-    padding-bottom: 4px;
-    font-size: 9px;
-    //font-style: italic;
-    text-decoration-color: grey;
-
-    .button {
-        color: black;
-
-        &:hover {
-            cursor: pointer;
-        }
-    }
-}
-
-.linkEditor-zoomFollow {
-    padding-left: 26px;
-    padding-right: 6.5px;
-    padding-bottom: 3.5px;
-    display: flex;
-
-    .linkEditor-zoomFollow-label {
-        text-decoration-color: black;
-        color: black;
-        line-height: 1.7;
-    }
-
-    .linkEditor-zoomFollow-input {
-        display: block;
-        width: 20px;
-    }
-}
-.linkEditor-deleteBtn {
-    padding-left: 3px;
-}
-
-.linkEditor-description {
-    padding-left: 26px;
-    padding-bottom: 3.5px;
-    display: flex;
-
-    .linkEditor-description-label {
-        text-decoration-color: black;
-        color: black;
-    }
-
-    .linkEditor-description-input {
-        display: flex;
-
-        .linkEditor-description-editing {
-            min-width: 85%;
-            //border: 1px solid grey;
-            //border-radius: 4px;
-            padding-left: 2px;
-            //margin-right: 4px;
-            color: black;
-            text-decoration-color: grey;
-        }
-
-        .linkEditor-description-add-button {
-            display: inline;
-            border-radius: 7px;
-            font-size: 9px;
-            background: black;
-            height: 80%;
-            color: white;
-            padding: 3px;
-            margin-left: 3px;
-
-            &:hover {
-                cursor: pointer;
-                background: grey;
-            }
-        }
-    }
-}
-
-.linkEditor-relationship-dropdown {
-    position: absolute;
-    width: 154px;
-    max-height: 90px;
-    overflow: auto;
-    background: white;
-
-    p {
-        padding: 3px;
-        cursor: pointer;
-        border: 1px solid $medium-gray;
-    }
-
-    p:hover {
-        background: $light-blue;
-    }
-}
-
-.linkEditor-followingDropdown {
-    padding-left: 26px;
-    padding-right: 6.5px;
-    padding-bottom: 15px;
-    display: flex;
-
-    &:hover {
-        cursor: pointer;
-    }
-
-    .linkEditor-followingDropdown-label {
-        color: black;
-        padding-right: 3px;
-    }
-
-    .linkEditor-followingDropdown-dropdown {
-        .linkEditor-followingDropdown-header {
-            border: 1px solid grey;
-            border-radius: 4px;
-            //background-color: rgb(236, 236, 236);
-            padding-left: 2px;
-            padding-right: 2px;
-            text-decoration-color: black;
-            color: rgb(94, 94, 94);
-
-            .linkEditor-followingDropdown-icon {
-                float: right;
-                color: black;
-            }
-        }
-
-        .linkEditor-followingDropdown-optionsList {
-            padding-left: 3px;
-            padding-right: 3px;
-
-            &:last-child {
-                border-bottom: none;
-            }
-
-            .linkEditor-followingDropdown-option {
-                border: 0.25px solid grey;
-                //background-color: rgb(236, 236, 236);
-                padding-left: 2px;
-                padding-right: 2px;
-                color: grey;
-                text-decoration-color: grey;
-                font-size: 9px;
-                border-top: none;
-
-                &:hover {
-                    background-color: rgb(187, 220, 231);
-                }
-            }
-        }
-    }
-}
-
-.linkEditor-button,
-.linkEditor-addbutton {
-    width: 15%;
-    border-radius: 7px;
-    font-size: 9px;
-    background: black;
-    padding: 3px;
-    height: 80%;
-    color: white;
-    text-align: center;
-    margin: auto;
-    margin-left: 3px;
-    > svg {
-        margin: auto;
-    }
-    &:disabled {
-        background-color: gray;
-    }
-}
-
-.linkEditor-addbutton {
-    margin-left: 0px;
-}
-
-.linkEditor-groupsLabel {
-    display: flex;
-    justify-content: space-between;
-}
-
-.linkEditor-group {
-    background-color: $light-gray;
-    padding: 6px;
-    margin: 3px 0;
-    border-radius: 3px;
-
-    .linkEditor-group-row {
-        display: flex;
-        margin-bottom: 3px;
-    }
-
-    .linkEditor-group-row-label {
-        margin-right: 6px;
-        display: inline-block;
-    }
-
-    .linkEditor-metadata-row {
-        display: flex;
-        justify-content: space-between;
-        margin-bottom: 6px;
-
-        .linkEditor-error {
-            border-color: red;
-        }
-
-        input {
-            width: calc(50% - 16px);
-            height: 20px;
-        }
-
-        button {
-            width: 20px;
-            height: 20px;
-            margin-left: 3px;
-            padding: 0;
-            font-size: 10px;
-        }
-    }
-}
-
-.linkEditor-dropdown {
-    width: 100%;
-    position: relative;
-    z-index: 999;
-
-    input {
-        width: 100%;
-    }
-
-    .linkEditor-options-wrapper {
-        width: 100%;
-        position: absolute;
-        top: 19px;
-        left: 0;
-        display: flex;
-        flex-direction: column;
-    }
-
-    .linkEditor-option {
-        background-color: $light-gray;
-        border: 1px solid $medium-gray;
-        border-top: 0;
-        padding: 3px;
-        cursor: pointer;
-
-        &:hover {
-            background-color: lightgray;
-        }
-
-        &.onDown {
-            background-color: gray;
-        }
-    }
-}
-
-.linkEditor-typeButton {
-    background-color: transparent;
-    color: $dark-gray;
-    height: 20px;
-    padding: 0 3px;
-    padding-bottom: 2px;
-    text-align: left;
-    text-transform: none;
-    letter-spacing: normal;
-    font-size: 12px;
-    font-weight: bold;
-    display: inline-block;
-    width: calc(100% - 40px);
-
-    &:hover {
-        background-color: $white;
-    }
-}
-
-.linkEditor-group-buttons {
-    height: 20px;
-    display: flex;
-    justify-content: flex-end;
-    margin-top: 5px;
-
-    .linkEditor-button {
-        margin-left: 3px;
-    }
-}
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
deleted file mode 100644
index 01e33708a..000000000
--- a/src/client/views/linking/LinkEditor.tsx
+++ /dev/null
@@ -1,454 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc';
-import { DateCast, StrCast, Cast, BoolCast, DocCast, NumCast } from '../../../fields/Types';
-import { LinkManager } from '../../util/LinkManager';
-import { undoBatch } from '../../util/UndoManager';
-import './LinkEditor.scss';
-import { LinkRelationshipSearch } from './LinkRelationshipSearch';
-import React = require('react');
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
-import { PresBox, PresEffect } from '../nodes/trails';
-
-interface LinkEditorProps {
-    sourceDoc: Doc;
-    linkDoc: Doc;
-    showLinks?: () => void;
-    hideback?: boolean;
-}
-@observer
-export class LinkEditor extends React.Component<LinkEditorProps> {
-    @observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
-    @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
-    @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom);
-    @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio);
-    @observable openDropdown: boolean = false;
-    @observable openEffectDropdown: boolean = false;
-    @observable private buttonColor: string = '';
-    @observable private relationshipButtonColor: string = '';
-    @observable private relationshipSearchVisibility: string = 'none';
-    @observable private searchIsActive: boolean = false;
-
-    //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
-
-    @undoBatch
-    setRelationshipValue = action((value: string) => {
-        if (LinkManager.currentLink) {
-            const prevRelationship = LinkManager.currentLink.linkRelationship as string;
-            LinkManager.currentLink.linkRelationship = value;
-            Doc.GetProto(LinkManager.currentLink).linkRelationship = value;
-            const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
-            const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes);
-            const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
-
-            // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
-            if (!linkRelationshipList?.includes(value)) {
-                linkRelationshipList.push(value);
-                linkRelationshipSizes.push(1);
-                const randColor = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';
-                linkColorList.push(randColor);
-                // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes
-            } else if (linkRelationshipList && value !== prevRelationship) {
-                const index = linkRelationshipList.indexOf(value);
-                //increment size of new relationship size
-                if (index !== -1 && index < linkRelationshipSizes.length) {
-                    const pvalue = linkRelationshipSizes[index];
-                    linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1;
-                }
-                //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
-                if (linkRelationshipList.includes(prevRelationship)) {
-                    const pindex = linkRelationshipList.indexOf(prevRelationship);
-                    if (pindex !== -1 && pindex < linkRelationshipSizes.length) {
-                        const pvalue = linkRelationshipSizes[pindex];
-                        linkRelationshipSizes[pindex] = Math.max(0, pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1);
-                    }
-                }
-            }
-            this.relationshipButtonColor = 'rgb(62, 133, 55)';
-            setTimeout(
-                action(() => (this.relationshipButtonColor = '')),
-                750
-            );
-            return true;
-        }
-    });
-
-    /**
-     * returns list of strings with possible existing relationships that contain what is currently in the input field
-     */
-    @action
-    getRelationshipResults = () => {
-        const query = this.relationship; //current content in input box
-        const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
-        if (linkRelationshipList) {
-            return linkRelationshipList.filter(rel => rel.includes(query));
-        }
-    };
-
-    /**
-     * toggles visibility of the relationship search results when the input field is focused on
-     */
-    @action
-    toggleRelationshipResults = () => {
-        this.relationshipSearchVisibility = this.relationshipSearchVisibility === 'none' ? 'block' : 'none';
-    };
-
-    @undoBatch
-    setDescripValue = action((value: string) => {
-        if (LinkManager.currentLink) {
-            Doc.GetProto(LinkManager.currentLink).description = value;
-            this.buttonColor = 'rgb(62, 133, 55)';
-            setTimeout(
-                action(() => (this.buttonColor = '')),
-                750
-            );
-            return true;
-        }
-    });
-
-    onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
-        if (e.key === 'Enter') {
-            this.setDescripValue(this.description);
-            document.getElementById('input')?.blur();
-        }
-        e.stopPropagation();
-    };
-
-    onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
-        if (e.key === 'Enter') {
-            this.setRelationshipValue(this.relationship);
-            document.getElementById('input')?.blur();
-        }
-        e.stopPropagation();
-    };
-
-    onDescriptionDown = () => this.setDescripValue(this.description);
-    onRelationshipDown = () => this.setRelationshipValue(this.relationship);
-
-    onBlur = () => {
-        //only hide the search results if the user clicks out of the input AND not on any of the search results
-        // i.e. if search is not active
-        if (!this.searchIsActive) {
-            this.toggleRelationshipResults();
-        }
-    };
-    onFocus = () => {
-        this.toggleRelationshipResults();
-    };
-    toggleSearchIsActive = () => {
-        this.searchIsActive = !this.searchIsActive;
-    };
-
-    @action
-    handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.description = e.target.value;
-    };
-    @action
-    handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.relationship = e.target.value;
-    };
-    @action
-    handleZoomFollowChange = () => {
-        this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom;
-    };
-    @action
-    handleAudioFollowChange = () => {
-        this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio;
-    };
-    @action
-    handleRelationshipSearchChange = (result: string) => {
-        this.setRelationshipValue(result);
-        this.toggleRelationshipResults();
-        this.relationship = result;
-    };
-    @computed
-    get editRelationship() {
-        //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        return (
-            <div className="linkEditor-description">
-                <div className="linkEditor-description-label">Relationship:</div>
-                <div className="linkEditor-description-input">
-                    <div className="linkEditor-description-editing">
-                        <input
-                            style={{ width: '100%' }}
-                            id="input"
-                            value={this.relationship}
-                            autoComplete={'off'}
-                            placeholder={'Enter link relationship'}
-                            onKeyDown={this.onRelationshipKey}
-                            onChange={this.handleRelationshipChange}
-                            onFocus={this.onFocus}
-                            onBlur={this.onBlur}></input>
-                        <LinkRelationshipSearch results={this.getRelationshipResults()} display={this.relationshipSearchVisibility} handleRelationshipSearchChange={this.handleRelationshipSearchChange} toggleSearch={this.toggleSearchIsActive} />
-                    </div>
-                    <div className="linkEditor-description-add-button" style={{ background: this.relationshipButtonColor }} onPointerDown={this.onRelationshipDown}>
-                        Set
-                    </div>
-                </div>
-            </div>
-        );
-    }
-    @computed
-    get editZoomFollow() {
-        //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        return (
-            <div className="linkEditor-zoomFollow">
-                <div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div>
-                <div className="linkEditor-zoomFollow-input">
-                    <div className="linkEditor-zoomFollow-editing">
-                        <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} />
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @computed
-    get editAudioFollow() {
-        //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        return (
-            <div className="linkEditor-zoomFollow">
-                <div className="linkEditor-zoomFollow-label">Play Target Audio:</div>
-                <div className="linkEditor-zoomFollow-input">
-                    <div className="linkEditor-zoomFollow-editing">
-                        <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} />
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @computed
-    get editDescription() {
-        return (
-            <div className="linkEditor-description">
-                <div className="linkEditor-description-label">Description:</div>
-                <div className="linkEditor-description-input">
-                    <div className="linkEditor-description-editing">
-                        <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input>
-                    </div>
-                    <div className="linkEditor-description-add-button" style={{ background: this.buttonColor }} onPointerDown={this.onDescriptionDown}>
-                        Set
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @action
-    changeDropdown = () => {
-        this.openDropdown = !this.openDropdown;
-    };
-
-    @undoBatch
-    changeFollowBehavior = action((follow: string) => {
-        this.openDropdown = false;
-        Doc.GetProto(this.props.linkDoc).followLinkLocation = follow;
-    });
-
-    @computed
-    get followingDropdown() {
-        return (
-            <div className="linkEditor-followingDropdown">
-                <div className="linkEditor-followingDropdown-label">Follow by:</div>
-                <div className="linkEditor-followingDropdown-dropdown">
-                    <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}>
-                        {StrCast(this.props.linkDoc.followLinkLocation, 'default')}
-                        <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
-                    </div>
-                    <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? '' : 'none' }}>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('default')}>
-                            Default
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:left')}>
-                            Always opening in new left pane
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:right')}>
-                            Always opening in new right pane
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:right')}>
-                            Always replacing right tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:left')}>
-                            Always replacing left tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('fullScreen')}>
-                            Always opening full screen
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add')}>
-                            Always opening in a new tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace')}>
-                            Replacing Tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('inPlace')}>
-                            Opening in Place
-                        </div>
-                        {this.props.linkDoc.linksToAnnotation ? (
-                            <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('openExternal')}>
-                                Always open in external page
-                            </div>
-                        ) : null}
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @computed get destinationAnchor() {
-        const ldoc = this.props.linkDoc;
-        if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) {
-            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2);
-            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1);
-        }
-        return LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc) ?? this.props.sourceDoc;
-    }
-    @action
-    changeEffectDropdown = () => {
-        this.openEffectDropdown = !this.openEffectDropdown;
-    };
-
-    @undoBatch
-    changeEffect = action((follow: string) => {
-        this.openEffectDropdown = false;
-        this.destinationAnchor.presEffect = follow;
-    });
-
-    @computed
-    get effectDropdown() {
-        return (
-            <div className="linkEditor-followingDropdown">
-                <div className="linkEditor-followingDropdown-label">Animation:</div>
-                <div className="linkEditor-followingDropdown-dropdown">
-                    <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}>
-                        {StrCast(this.destinationAnchor.presEffect, 'default')}
-                        <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openEffectDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
-                    </div>
-                    <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openEffectDropdown ? '' : 'none' }}>
-                        {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
-                            <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeEffect(effect.toString())}>
-                                {effect.toString()}
-                            </div>
-                        ))}
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    autoMove = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
-    };
-
-    showAnchor = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
-    };
-
-    showLink = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
-    };
-
-    deleteLink = (e: React.PointerEvent): void => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
-    };
-
-    render() {
-        const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-
-        return !destination ? null : (
-            <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
-                <div className="linkEditor-info">
-                    {!this.props.showLinks ? null : (
-                        <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top">
-                            <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}>
-                                <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
-                            </button>
-                        </Tooltip>
-                    )}
-                    <p className="linkEditor-linkedTo">
-                        Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b>
-                    </p>
-                    <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
-                        <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                <div className="linkEditor-moreInfo">
-                    {this.props.linkDoc.author ? (
-                        <>
-                            {' '}
-                            <b>Author:</b> {StrCast(this.props.linkDoc.author)}
-                        </>
-                    ) : null}
-                    {this.props.linkDoc.creationDate ? (
-                        <>
-                            {' '}
-                            <b>Creation Date:</b>
-                            {DateCast(this.props.linkDoc.creationDate).toString()}
-                        </>
-                    ) : null}
-                </div>
-                {this.editDescription}
-                {this.editRelationship}
-                {this.editZoomFollow}
-                {this.editAudioFollow}
-                <div className="linkEditor-description">
-                    Show Anchor:
-                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}>
-                        <div
-                            className="linkEditor-button"
-                            style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue                                       */ }}
-                            onPointerDown={this.showAnchor}
-                            onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                <div className="linkEditor-description">
-                    Show Link Line:
-                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}>
-                        <div
-                            className="linkEditor-button"
-                            style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
-                            onPointerDown={this.showLink}
-                            onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                <div className="linkEditor-description">
-                    Freeze Anchor:
-                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}>
-                        <div
-                            className="linkEditor-button"
-                            style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
-                            onPointerDown={this.autoMove}
-                            onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                {this.followingDropdown}
-                {this.effectDropdown}
-                {PresBox.inputter('0.1', '0.1', '10', NumCast(this.destinationAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.destinationAnchor.presTransition = timeInMS)))}
-                <div
-                    className={'slider-headers'}
-                    style={{
-                        marginLeft: 30,
-                        marginRight: 30,
-                        display: 'grid',
-                        justifyContent: 'space-between',
-                        width: '100%',
-                        gridTemplateColumns: 'auto auto auto',
-                    }}>
-                    <div className="slider-text">Fast</div>
-                    <div className="slider-text">Medium</div>
-                    <div className="slider-text">Slow</div>
-                </div>{' '}
-            </div>
-        );
-    }
-}
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 0c46a6d96..c9112eec3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,14 +1,13 @@
-import { action, computed, observable } from 'mobx';
+import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../../fields/Doc';
+import { DocCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
-import { LinkEditor } from './LinkEditor';
 import './LinkMenu.scss';
 import { LinkMenuGroup } from './LinkMenuGroup';
 import React = require('react');
-import { emptyFunction } from '../../../Utils';
 
 interface Props {
     docView: DocumentView;
@@ -23,15 +22,9 @@ interface Props {
 @observer
 export class LinkMenu extends React.Component<Props> {
     _editorRef = React.createRef<HTMLDivElement>();
-    @observable _editingLink?: Doc;
     @observable _linkMenuRef = React.createRef<HTMLDivElement>();
 
-    clear = !this.props.clearLinkEditor
-        ? undefined
-        : action(() => {
-              this.props.clearLinkEditor?.();
-              this._editingLink = undefined;
-          });
+    clear = () => this.props.clearLinkEditor?.();
 
     componentDidMount() {
         this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true);
@@ -43,7 +36,7 @@ export class LinkMenu extends React.Component<Props> {
     onPointerDown = action((e: PointerEvent) => {
         LinkDocPreview.Clear();
         if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) {
-            this.clear?.();
+            this.clear();
         }
     });
 
@@ -54,34 +47,20 @@ export class LinkMenu extends React.Component<Props> {
      */
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         const linkItems = Array.from(groups.entries()).map(group => (
-            <LinkMenuGroup
-                key={group[0]}
-                itemHandler={this.props.itemHandler}
-                docView={this.props.docView}
-                sourceDoc={this.props.docView.props.Document}
-                group={group[1]}
-                groupType={group[0]}
-                clearLinkEditor={this.clear}
-                showEditor={action(linkDoc => (this._editingLink = linkDoc))}
-            />
+            <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} />
         ));
 
         return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
     };
 
     render() {
-        const sourceDoc = this.props.docView.props.Document;
+        const sourceDoc = this.props.docView.rootDoc;
+        const sourceAnchor = this.props.docView.anchorViewDoc ?? sourceDoc;
         const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds());
 
         return (
             <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}>
-                {this._editingLink ? (
-                    <div className="linkMenu-listEditor">
-                        <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} />
-                    </div>
-                ) : (
-                    <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))}</div>
-                )}
+                <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div>
             </div>
         );
     }
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 9d2082e21..d02a1c4eb 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -2,19 +2,19 @@ import { observer } from 'mobx-react';
 import { observable, action } from 'mobx';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { Cast } from '../../../fields/Types';
+import { Cast, DocCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { DocumentView } from '../nodes/DocumentView';
 import './LinkMenu.scss';
 import { LinkMenuItem } from './LinkMenuItem';
 import React = require('react');
+import { DocumentType } from '../../documents/DocumentTypes';
 
 interface LinkMenuGroupProps {
     sourceDoc: Doc;
     group: Doc[];
     groupType: string;
     clearLinkEditor?: () => void;
-    showEditor: (linkDoc: Doc) => void;
     docView: DocumentView;
     itemHandler?: (doc: Doc) => void;
 }
@@ -44,25 +44,33 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
     render() {
         const set = new Set<Doc>(this.props.group);
         const groupItems = Array.from(set.keys()).map(linkDoc => {
-            const destination =
-                LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) ||
-                LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
-            if (destination && this.props.sourceDoc) {
-                return (
-                    <LinkMenuItem
-                        key={linkDoc[Id]}
-                        itemHandler={this.props.itemHandler}
-                        groupType={this.props.groupType}
-                        docView={this.props.docView}
-                        linkDoc={linkDoc}
-                        sourceDoc={this.props.sourceDoc}
-                        destinationDoc={destination}
-                        clearLinkEditor={this.props.clearLinkEditor}
-                        showEditor={this.props.showEditor}
-                        menuRef={this._menuRef}
-                    />
-                );
-            }
+            const sourceDoc =
+                this.props.docView.anchorViewDoc ??
+                (this.props.docView.rootDoc.type === DocumentType.LINK //
+                    ? this.props.docView.props.LayoutTemplateString?.includes('anchor1')
+                        ? DocCast(linkDoc.anchor1)
+                        : DocCast(linkDoc.anchor2)
+                    : this.props.sourceDoc);
+            const destDoc = !sourceDoc
+                ? undefined
+                : this.props.docView.rootDoc.type === DocumentType.LINK
+                ? this.props.docView.props.LayoutTemplateString?.includes('anchor1')
+                    ? DocCast(linkDoc.anchor2)
+                    : DocCast(linkDoc.anchor1)
+                : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) || LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
+            return !destDoc || !sourceDoc ? null : (
+                <LinkMenuItem
+                    key={linkDoc[Id]}
+                    itemHandler={this.props.itemHandler}
+                    groupType={this.props.groupType}
+                    docView={this.props.docView}
+                    linkDoc={linkDoc}
+                    sourceDoc={sourceDoc}
+                    destinationDoc={destDoc}
+                    clearLinkEditor={this.props.clearLinkEditor}
+                    menuRef={this._menuRef}
+                />
+            );
         });
 
         return (
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index c3705b0e1..fb4c6873e 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -18,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
 import React = require('react');
+import { SelectionManager } from '../../util/SelectionManager';
 
 interface LinkMenuItemProps {
     groupType: string;
@@ -26,7 +27,6 @@ interface LinkMenuItemProps {
     sourceDoc: Doc;
     destinationDoc: Doc;
     clearLinkEditor?: () => void;
-    showEditor: (linkDoc: Doc) => void;
     menuRef: React.Ref<HTMLDivElement>;
     itemHandler?: (doc: Doc) => void;
 }
@@ -100,10 +100,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
             },
             emptyFunction,
             action(() => {
+                SelectionManager.SelectView(this.props.docView, false);
                 if ((SettingsManager.propertiesWidth ?? 0) < 100) {
                     SettingsManager.propertiesWidth = 250;
                 }
-                //this.props.showEditor(this.props.linkDoc);
             })
         );
     };
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 627487a9e..99fa62fa7 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -305,7 +305,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     top: 0,
                     pointerEvents: 'none',
                 }}>
-                <Tooltip title={!DocumentLinksButton.LinkEditorDocView ? <div className="dash-tooltip">{title}</div> : <></>}>{this.linkButtonInner}</Tooltip>
+                {!DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2729a5047..a8bea61c9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
 import { observer, renderReporter } from 'mobx-react';
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -11,7 +11,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
@@ -1156,7 +1156,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     anchorPanelHeight = () => this.props.PanelHeight() || 1;
     anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
         // prettier-ignore
-        switch (property) {
+        switch (property.split(':')[0]) {
             case StyleProp.ShowTitle:     return '';
             case StyleProp.PointerEvents: return 'none';
             case StyleProp.LinkSource:    return this.props.Document; // pass the LinkSource to the LinkAnchorBox
@@ -1188,7 +1188,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         TraceMobx();
         if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
         if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null;
-        const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
+        const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay);
         return filtered.map((link, i) => (
             <div className="documentView-anchorCont" key={link[Id]}>
                 <DocumentView
@@ -1415,7 +1415,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
-        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? Doc.HighlightBrush.linkFollowEffect, this.rootDoc) : renderDoc;
+        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? this.rootDoc[AnimationSym], this.rootDoc) : renderDoc;
         return (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
@@ -1535,8 +1535,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale;
 
     @computed get linkCountView() {
-        return (this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton) &&
-            DocumentLinksButton.LinkEditorDocView?.rootDoc !== this.rootDoc ? null : (
+        return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton ? null : (
             <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />
         );
     }
@@ -1654,6 +1653,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
 
+    @computed get anchorViewDoc() {
+        return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : this.rootDoc;
+    }
     docViewPathFunc = () => this.docViewPath;
     isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
     select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index be9565452..e89076c1f 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../../fields/Doc';
@@ -7,20 +6,17 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
-import { SelectionManager } from '../../util/SelectionManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxBaseComponent } from '../DocComponent';
-import { LinkEditor } from '../linking/LinkEditor';
 import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkAnchorBox.scss';
 import { LinkDocPreview } from './LinkDocPreview';
 import React = require('react');
-import { OpenWhere } from './DocumentView';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import { LinkManager } from '../../util/LinkManager';
+import globalCssVariables = require('../global/globalCssVariables.scss');
+import { SelectionManager } from '../../util/SelectionManager';
 
 @observer
 export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -34,15 +30,23 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
     _timeout: NodeJS.Timeout | undefined;
     @observable _x = 0;
     @observable _y = 0;
-    @observable _selected = false;
-    @observable _editing = false;
-    @observable _forceOpen = false;
 
     onPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
+        const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
+        setupMoveUpEvents(
+            this,
+            e,
+            this.onPointerMove,
+            emptyFunction,
+            (e, doubleTap) => {
+                if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
+                else this.props.select(false);
+            },
+            false
+        );
     };
     onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
-        const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
+        const cdiv = this._ref?.current?.parentElement;
         if (!this._isOpen && cdiv) {
             const bounds = cdiv.getBoundingClientRect();
             const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
@@ -60,58 +64,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         return false;
     });
-    @action
-    onClick = (e: React.MouseEvent) => {
-        if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) {
-            this.props.select(false);
-        }
-        if (!this._doubleTap && !e.ctrlKey && e.button < 2) {
-            const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
-            this._editing = true;
-            anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false);
-            if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) {
-                this._timeout = setTimeout(
-                    action(() => {
-                        LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
-                        this._editing = false;
-                    }),
-                    300 - (Date.now() - this._lastTap)
-                );
-                e.stopPropagation();
-            }
-        } else {
-            this._timeout && clearTimeout(this._timeout);
-            this._timeout = undefined;
-            this._doubleTap = false;
-            this.openLinkEditor(e);
-            e.stopPropagation();
-        }
-    };
 
-    openLinkDocOnRight = (e: React.MouseEvent) => {
-        this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
-    };
-    openLinkTargetOnRight = (e: React.MouseEvent) => {
-        const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null));
-        alias._isLinkButton = undefined;
-        alias.layoutKey = 'layout';
-        this.props.addDocTab(alias, OpenWhere.addRight);
-    };
-    @action
-    openLinkEditor = action((e: React.MouseEvent) => {
-        SelectionManager.DeselectAll();
-        this._editing = this._forceOpen = true;
-    });
-
-    specificContextMenu = (e: React.MouseEvent): void => {
-        const funcs: ContextMenuProps[] = [];
-        funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' });
-        funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' });
-        funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' });
-        funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' });
-
-        ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
-    };
+    specificContextMenu = (e: React.MouseEvent): void => {};
 
     render() {
         TraceMobx();
@@ -122,22 +76,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor');
         const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1';
         const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
-
         const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
-        const flyout = (
-            <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}>
-                <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} />
-                {!this._forceOpen ? null : (
-                    <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}>
-                        <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} />
-                    </div>
-                )}
-            </div>
-        );
+        const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor1') ? 'anchor1' : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor2') ? 'anchor2' : '';
         return (
             <div
+                ref={this._ref}
+                title={targetTitle}
                 className={`linkAnchorBox-cont${small ? '-small' : ''}`}
-                //onPointerLeave={} //LinkDocPreview.Clear}
                 onPointerEnter={e =>
                     LinkDocPreview.SetLinkInfo({
                         docProps: this.props,
@@ -149,24 +94,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     })
                 }
                 onPointerDown={this.onPointerDown}
-                onClick={this.onClick}
-                title={targetTitle}
                 onContextMenu={this.specificContextMenu}
-                ref={this._ref}
                 style={{
+                    border: selView && this.rootDoc[selView] === this.rootDoc[this.fieldKey] ? `solid ${globalCssVariables.MEDIUM_GRAY} 2px` : undefined,
                     background,
                     left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
                     top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
                     transform: `scale(${anchorScale})`,
-                }}>
-                {!this._editing && !this._forceOpen ? null : (
-                    <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => (this._isOpen = true)} onClose={action(() => (this._isOpen = this._forceOpen = this._editing = false))}>
-                        <span className="linkAnchorBox-button">
-                            <FontAwesomeIcon icon={'eye'} size={'lg'} />
-                        </span>
-                    </Flyout>
-                )}
-            </div>
+                }}
+            />
         );
     }
 }
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c10751698..c6cabe269 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -94,6 +94,8 @@ export function DocListCastOrNull(field: FieldResult) {
 
 export const WidthSym = Symbol('Width');
 export const HeightSym = Symbol('Height');
+export const AnimationSym = Symbol('Animation');
+export const HighlightSym = Symbol('Highlight');
 export const DataSym = Symbol('Data');
 export const LayoutSym = Symbol('Layout');
 export const FieldsSym = Symbol('Fields');
@@ -329,6 +331,8 @@ export class Doc extends RefField {
     @observable private ___fieldKeys: any = {};
     @observable public [AclSym]: { [key: string]: symbol } = {};
     @observable public [DirectLinksSym]: Set<Doc> = new Set();
+    @observable public [AnimationSym]: Opt<Doc>;
+    @observable public [HighlightSym]: boolean = false;
 
     private [UpdatingFromServer]: boolean = false;
     private [ForceServerWrite]: boolean = false;
@@ -1261,53 +1265,55 @@ export namespace Doc {
     }
 
     export function linkFollowUnhighlight() {
-        Doc.UnhighlightAll();
+        UnhighlightWatchers.length = 0;
+        highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
-        runInAction(() => (HighlightBrush.linkFollowEffect = undefined));
     }
 
-    let _lastDate = 0;
+    let UnhighlightWatchers: (() => void)[] = [];
+    let UnhighlightTimer: any;
+    export function AddUnlightWatcher(watcher: () => void) {
+        if (UnhighlightTimer) {
+            UnhighlightWatchers.push(watcher);
+        } else watcher();
+    }
     export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
-        //linkFollowUnhighlight();
-        // runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = undefined));
-        // setTimeout(() => runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect)));
-        runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect));
-        (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
+        linkFollowUnhighlight();
+        (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
         document.addEventListener('pointerdown', linkFollowUnhighlight);
-        const lastDate = (_lastDate = Date.now());
-        window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
+        if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
+        UnhighlightTimer = window.setTimeout(() => {
+            UnhighlightWatchers.forEach(watcher => watcher());
+            linkFollowUnhighlight();
+            UnhighlightTimer = 0;
+        }, 5000);
     }
 
-    export class HighlightBrush {
-        @observable HighlightedDoc: Map<Doc, boolean> = new Map();
-        @observable static linkFollowEffect: Doc | undefined;
-    }
-    const highlightManager = new HighlightBrush();
+    var highlightedDocs = new Set<Doc>();
     export function IsHighlighted(doc: Doc) {
         if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
-        return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
+        return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
     }
-    export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
+    export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
         runInAction(() => {
-            highlightManager.HighlightedDoc.set(doc, true);
-            dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
+            doc[AnimationSym] = presEffect;
+            highlightedDocs.add(doc);
+            doc[HighlightSym] = true;
+            if (dataAndDisplayDocs) {
+                highlightedDocs.add(Doc.GetProto(doc));
+                Doc.GetProto(doc)[HighlightSym] = true;
+            }
         });
     }
     export function UnHighlightDoc(doc: Doc) {
         runInAction(() => {
-            highlightManager.HighlightedDoc.set(doc, false);
-            highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
+            highlightedDocs.delete(doc);
+            highlightedDocs.delete(Doc.GetProto(doc));
+            doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
+            doc[AnimationSym] = undefined;
         });
     }
-    export function UnhighlightAll() {
-        const mapEntries = highlightManager.HighlightedDoc.keys();
-        let docEntry: IteratorResult<Doc>;
-        while (!(docEntry = mapEntries.next()).done) {
-            const targetDoc = docEntry.value;
-            targetDoc && Doc.UnHighlightDoc(targetDoc);
-        }
-    }
     export function UnBrushAllDocs() {
         brushManager.BrushedDoc.clear();
     }
-- 
cgit v1.2.3-70-g09d2


From e17b1bdb09bfcadc717e687b09d2c18596341a10 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 9 Feb 2023 21:15:58 -0500
Subject: fixed childLayoutString to work.  made images capable of fitWidth.
 fixed animating data field pres changes.  fixed lightbox to ignore
 annotations on collections.  fixed double-click on icon to open in lightbox.
 added options for turning off ink labels, and opening ink in lightbox. fixed
 closing ink strokes by dragging. fixed drawing ink to use coord sys of
 starting point and to render ink the correct width and to honor
 GestureOverlay mode properly. .

---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/CurrentUserUtils.ts                |  6 +-
 src/client/util/SettingsManager.tsx                |  4 ++
 src/client/views/DocumentDecorations.tsx           | 13 ++--
 src/client/views/GestureOverlay.tsx                | 22 ++++---
 src/client/views/GlobalKeyHandler.ts               | 29 ++++++++-
 src/client/views/InkTranscription.tsx              |  2 +-
 src/client/views/InkingStroke.tsx                  | 12 ++--
 src/client/views/LightboxView.tsx                  | 64 +++++++++----------
 src/client/views/PropertiesButtons.tsx             | 10 +++
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/StyleProvider.tsx                 |  4 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 src/client/views/collections/TabDocView.tsx        | 14 ++---
 src/client/views/collections/TreeView.tsx          |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 66 +++++++++++--------
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/nodes/DocumentView.tsx            | 21 ++++---
 src/client/views/nodes/ImageBox.tsx                | 73 +++++++++++++++++-----
 src/client/views/nodes/LinkAnchorBox.tsx           |  1 +
 src/client/views/nodes/MapBox/MapBox.tsx           |  4 +-
 src/client/views/nodes/WebBox.tsx                  | 11 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  1 -
 src/client/views/nodes/trails/PresBox.tsx          |  4 ++
 src/client/views/pdf/AnchorMenu.tsx                |  1 +
 src/client/views/pdf/PDFViewer.tsx                 | 17 ++---
 src/fields/util.ts                                 |  2 +-
 27 files changed, 257 insertions(+), 140 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 692d09629..3faa6e11d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -149,6 +149,8 @@ export class DocumentOptions {
     _height?: NUMt = new NumInfo('displayed height of document');
     _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)');
     _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)');
+    _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers');
+    _nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers');
     _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
     _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
     _fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
@@ -848,7 +850,7 @@ export namespace Docs {
 
         export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             const imgField = url instanceof ImageField ? url : new ImageField(url);
-            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
+            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { _nativeDimModifiable: false, _nativeHeightUnfrozen: false, title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function PresDocument(options: DocumentOptions = {}) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5f183cf91..f678c8936 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -183,7 +183,7 @@ export class CurrentUserUtils {
             const allopts = {system: true, ...opts};
             return DocUtils.AssignScripts(  (curIcon?.iconTemplate === opts.iconTemplate ? 
                       DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
-                {onClick:"deiconifyView(documentView)"});
+                {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView"});
         };
         const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
             textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
@@ -803,7 +803,9 @@ export class CurrentUserUtils {
         doc._showLabel ?? (doc._showLabel = true);
         doc.textAlign ?? (doc.textAlign = "left");
         doc.activeTool = InkTool.None;
-        doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
+        doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
+        doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
+        doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
         doc.activeInkWidth ?? (doc.activeInkWidth = 1);
         doc.activeInkBezier ?? (doc.activeInkBezier = "0");
         doc.activeFillColor ?? (doc.activeFillColor = "");
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 179a1ac39..6c823e80a 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -199,6 +199,10 @@ export class SettingsManager extends React.Component<{}> {
                     <input type="checkbox" onChange={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} checked={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} />
                     <div className="preferences-check">Hide Labels In Ink Shapes</div>
                 </div>
+                <div>
+                    <input type="checkbox" onChange={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} checked={BoolCast(Doc.UserDoc().openInkInLightbox)} />
+                    <div className="preferences-check">Open Ink Docs in Lightbox</div>
+                </div>
             </div>
         );
     }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d1f0bf2ac..41f4a17fb 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -581,7 +581,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     }
                     let actualdW = Math.max(width + dW * scale, 20);
                     let actualdH = Math.max(height + dH * scale, 20);
-                    const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
+                    const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false;
+                    const fixedAspect = nwidth && nheight && (!doc._fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
                     if (fixedAspect) {
                         if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
                             if (dragRight && modifyNativeDim) {
@@ -589,7 +590,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc);
                                 }
                             } else {
-                                if (!doc._fitWidth) {
+                                if (!doc._fitWidth || preserveNativeDim) {
                                     actualdH = (nheight / nwidth) * actualdW;
                                     doc._height = actualdH;
                                 } else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
@@ -597,11 +598,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             doc._width = actualdW;
                         } else {
                             if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) {
-                                // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height.  But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
+                                // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height.  But they are marked to allow their nativeHeight
+                                // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match
+                                // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
                                 doc._nativeHeight = (actualdH / (doc._height || 1)) * Doc.NativeHeight(doc);
                                 doc._autoHeight = false;
                             } else {
-                                if (!doc._fitWidth) {
+                                if (!doc._fitWidth || preserveNativeDim) {
                                     actualdW = (nwidth / nheight) * actualdH;
                                     doc._width = actualdW;
                                 } else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
@@ -615,7 +618,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
                         const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI);
 
-                        const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
+                        const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
                         dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
                         dW && (doc._width = actualdW);
                         dH && (doc._autoHeight = false);
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e3328fb4c..6058eaaf9 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,9 +1,9 @@
 import React = require('react');
 import * as fitCurve from 'fit-curve';
-import { action, computed, observable, runInAction, trace } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../fields/Doc';
 import { InkData, InkTool } from '../../fields/InkField';
-import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, FieldValue, NumCast } from '../../fields/Types';
 import MobileInkOverlay from '../../mobile/MobileInkOverlay';
@@ -14,7 +14,6 @@ import { CognitiveServices } from '../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SelectionManager } from '../util/SelectionManager';
 import { Transform } from '../util/Transform';
 import './GestureOverlay.scss';
 import {
@@ -39,7 +38,6 @@ import { RadialMenu } from './nodes/RadialMenu';
 import HorizontalPalette from './Palette';
 import { Touchable } from './Touchable';
 import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
-import { observer } from 'mobx-react';
 
 interface GestureOverlayProps {
     isActive: boolean;
@@ -47,6 +45,7 @@ interface GestureOverlayProps {
 @observer
 export class GestureOverlay extends Touchable<GestureOverlayProps> {
     static Instance: GestureOverlay;
+    static Instances: GestureOverlay[] = [];
 
     @observable public InkShape: Opt<GestureUtils.Gestures>;
     @observable public SavedColor?: string;
@@ -66,6 +65,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     @observable private _clipboardDoc?: JSX.Element;
     @observable private _possibilities: JSX.Element[] = [];
 
+    public static DownDocView: DocumentView | undefined;
+
     @computed private get height(): number {
         return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
     }
@@ -89,7 +90,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     constructor(props: any) {
         super(props);
 
-        GestureOverlay.Instance = this;
+        GestureOverlay.Instances.push(this);
     }
 
     static setupThumbButtons(doc: Doc) {
@@ -154,7 +155,13 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         }
         return Cast(userDoc.thumbDoc, Doc);
     }
+
+    componentWillUnmount() {
+        GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1);
+        GestureOverlay.Instance = GestureOverlay.Instances.lastElement();
+    }
     componentDidMount = () => {
+        GestureOverlay.Instance = this;
         this._thumbDoc = FieldValue(Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc));
         this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc));
     };
@@ -627,6 +634,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     }
     @action
     onPointerUp = (e: PointerEvent) => {
+        GestureOverlay.DownDocView = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
@@ -906,8 +914,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     }
 
     @computed get elements() {
-        const selView = SelectionManager.Views().lastElement();
-        const width = (Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1)) / (selView?.props.ScreenToLocalTransform().Scale || 1);
+        const selView = GestureOverlay.DownDocView;
+        const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1); // * (selView?.props.ScreenToLocalTransform().Scale || 1);
         const rect = this._overlayRef.current?.getBoundingClientRect();
         const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true);
         B.left = B.left - width / 2;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 5e700e281..6c8a078ec 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,12 +1,12 @@
 import { random } from 'lodash';
-import { action, observable, runInAction } from 'mobx';
+import { action, runInAction } from 'mobx';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
-import { Cast, DocCast, PromiseValue } from '../../fields/Types';
+import { Cast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -18,6 +18,7 @@ import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormViewChrome } from './collections/CollectionMenu';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
 import { ContextMenu } from './ContextMenu';
@@ -103,6 +104,20 @@ export class KeyManager {
 
                 const groupings = SelectionManager.Views().slice();
                 const randomGroup = random(0, 1000);
+                const collectionView = groupings.reduce(
+                    (col, g) => (col === null || g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView === col ? g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined),
+                    null as null | undefined | CollectionFreeFormView
+                );
+                if (collectionView) {
+                    UndoManager.RunInBatch(() => {
+                        collectionView._marqueeViewRef.current?.collection(
+                            e,
+                            true,
+                            groupings.map(g => g.rootDoc)
+                        );
+                    }, 'grouping');
+                    break;
+                }
                 UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group');
                 SelectionManager.DeselectAll();
                 break;
@@ -192,6 +207,16 @@ export class KeyManager {
             case 'arrowdown':
                 UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 10)), 'nudge down');
                 break;
+            case 'g':
+                if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
+                    return { stopPropagation: false, preventDefault: false };
+                }
+
+                const groupings = SelectionManager.Views().slice();
+                const randomGroup = random(0, 1000);
+                UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group');
+                SelectionManager.DeselectAll();
+                break;
         }
 
         return {
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index bf0e8081d..246b887a6 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -67,7 +67,7 @@ export class InkTranscription extends React.Component {
                 : null;
         }
 
-        r.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
+        r?.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
 
         return (this._mathRef = r);
     };
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 7a5151634..d7e8b1c05 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -54,7 +54,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(InkingStroke, fieldStr);
     }
     public static IsClosed(inkData: InkData) {
-        return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
+        return inkData?.length && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
     }
     private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
     private _disposers: { [key: string]: IReactionDisposer } = {};
@@ -264,9 +264,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
             .map(p => ({ X: p[0], Y: p[1] }));
         const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
 
-        this._nearestT = nearestT;
-        this._nearestSeg = nearestSeg;
-        this._nearestScrPt = nearestPt;
+        if (distance < 40) {
+            this._nearestT = nearestT;
+            this._nearestSeg = nearestSeg;
+            this._nearestScrPt = nearestPt;
+        } else {
+            this._nearestT = this._nearestSeg = this._nearestScrPt = undefined;
+        }
     };
 
     /**
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 3627aa783..e531bf71c 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -27,6 +27,13 @@ interface LightboxViewProps {
     maxBorder: number[];
 }
 
+type LightboxSavedState = {
+    panX: Opt<number>;
+    panY: Opt<number>;
+    scale: Opt<number>;
+    scrollTop: Opt<number>;
+    layoutKey: Opt<string>;
+};
 @observer
 export class LightboxView extends React.Component<LightboxViewProps> {
     @computed public static get LightboxDoc() {
@@ -34,21 +41,22 @@ export class LightboxView extends React.Component<LightboxViewProps> {
     }
     private static LightboxDocTemplate = () => LightboxView._layoutTemplate;
     @observable private static _layoutTemplate: Opt<Doc>;
+    @observable private static _layoutTemplateString: Opt<string>;
     @observable private static _doc: Opt<Doc>;
     @observable private static _docTarget: Opt<Doc>;
     @observable private static _docFilters: string[] = []; // filters
-    private static _savedState: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }>;
+    private static _savedState: Opt<LightboxSavedState>;
     private static _history: Opt<{ doc: Doc; target?: Doc }[]> = [];
     @observable private static _future: Opt<Doc[]> = [];
     @observable private static _docView: Opt<DocumentView>;
-    private static openInTabFunc: any;
-    static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }> }[] = [];
-    @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc) {
+    static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = [];
+    @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
         if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) {
-            this.LightboxDoc._panX = this._savedState.panX;
-            this.LightboxDoc._panY = this._savedState.panY;
-            this.LightboxDoc._scrollTop = this._savedState.scrollTop;
-            this.LightboxDoc._viewScale = this._savedState.scale;
+            if (this._savedState.panX !== undefined) this.LightboxDoc._panX = this._savedState.panX;
+            if (this._savedState.panY !== undefined) this.LightboxDoc._panY = this._savedState.panY;
+            if (this._savedState.scrollTop !== undefined) this.LightboxDoc._scrollTop = this._savedState.scrollTop;
+            if (this._savedState.scale !== undefined) this.LightboxDoc._viewScale = this._savedState.scale;
+            this.LightboxDoc.layoutKey = this._savedState.layoutKey;
         }
         if (!doc) {
             this._docFilters && (this._docFilters.length = 0);
@@ -69,10 +77,11 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
             if (doc !== LightboxView.LightboxDoc) {
                 this._savedState = {
-                    panX: Cast(doc._panX, 'number', null),
-                    panY: Cast(doc._panY, 'number', null),
-                    scale: Cast(doc._viewScale, 'number', null),
-                    scrollTop: Cast(doc._scrollTop, 'number', null),
+                    layoutKey: StrCast(doc.layoutKey),
+                    panX: Cast(doc.panX, 'number', null),
+                    panY: Cast(doc.panY, 'number', null),
+                    scale: Cast(doc.viewScale, 'number', null),
+                    scrollTop: Cast(doc.scrollTop, 'number', null),
                 };
             }
         }
@@ -87,7 +96,10 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             ];
         }
         this._doc = doc;
-        this._layoutTemplate = layoutTemplate;
+        this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined;
+        if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) {
+            doc.layoutKey = layoutTemplate;
+        }
         this._docTarget = target || doc;
 
         return true;
@@ -132,7 +144,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`);
         }
     }
-    public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc, openInTabFunc?: any) => {
+    public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
         if (location !== OpenWhere.lightbox) {
             const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
             if (inPlaceView) {
@@ -140,12 +152,13 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 return true;
             }
         }
-        LightboxView.openInTabFunc = openInTabFunc;
         SelectionManager.DeselectAll();
         return LightboxView.SetLightboxDoc(
             doc,
             undefined,
-            [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '-annotations']), ...(LightboxView._future ?? [])].sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)),
+            [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '-annotations']).filter(anno => anno.annotationOn !== doc), ...(LightboxView._future ?? [])].sort(
+                (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)
+            ),
             layoutTemplate
         );
     };
@@ -193,8 +206,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
         if (docView) {
             LightboxView._docTarget = target;
-            if (!target) docView.ComponentView?.shrinkWrap?.();
-            else docView.focus(target, { willPanZoom: true, zoomScale: 0.9 });
+            target && docView.focus(target, { willPanZoom: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
@@ -218,7 +230,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 .filter(doc => doc)
                 .map(doc => doc!);
             LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
-            TabDocView.PinDoc(coll, { hidePresBox: true });
         }
     };
 
@@ -251,19 +262,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
 
                     <GestureOverlay isActive={true}>
                         <DocumentView
-                            ref={action((r: DocumentView | null) => {
-                                LightboxView._docView = r !== null ? r : undefined;
-                                r &&
-                                    setTimeout(
-                                        action(() => {
-                                            const target = LightboxView._docTarget;
-                                            const doc = LightboxView._doc;
-                                            //const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
-                                            //if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
-                                            //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here?  it breaks smooth navigation in lightbox using 'next' button
-                                        })
-                                    );
-                            })}
+                            ref={action((r: DocumentView | null) => (LightboxView._docView = r !== null ? r : undefined))}
                             Document={LightboxView.LightboxDoc}
                             DataDoc={undefined}
                             PanelWidth={this.lightboxWidth}
@@ -332,7 +331,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     onClick={e => {
                         e.stopPropagation();
                         CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
-                        //LightboxView.openInTabFunc(LightboxView._docTarget || LightboxView._doc!, "inPlace");
                         SelectionManager.DeselectAll();
                         LightboxView.SetLightboxDoc(undefined);
                     }}>
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 66c3ed439..65a950711 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -77,6 +77,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc)
         );
     }
+    @computed get hideImageButton() {
+        return this.propertyToggleBtn(
+            'Background',
+            '_hideImage',
+            on => (on ? 'Show Image' : 'Show Background'),
+            on => 'portrait'
+        );
+    }
     @computed get clustersButton() {
         return this.propertyToggleBtn(
             'Clusters',
@@ -383,6 +391,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
         const isText = layoutField instanceof RichTextField;
         const isInk = layoutField instanceof InkField;
+        const isImage = layoutField instanceof ImageField;
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
         const isCollection = this.selectedDoc?.type === DocumentType.COL;
         //TODO: will likely need to create separate note-taking view type here
@@ -410,6 +419,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.inPlaceContainerButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
                 {toggle(this.maskButton, { display: !isInk ? 'none' : '' })}
+                {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })}
                 {toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })}
                 {toggle(this.gridButton, { display: !isCollection ? 'none' : '' })}
                 {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 411f51d84..a2bc37095 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -143,7 +143,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         : layoutDoc._fitWidth
                         ? !Doc.NativeHeight(this.dataDoc)
                             ? NumCast(this.props.height)
-                            : Math.min((this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))
+                            : Math.min((this.docWidth() * Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))
                         : NumCast(layoutDoc._height) || 50
                 )
             );
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index ece224c68..3cb920ba0 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -269,9 +269,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                 default:
                     return doc.z
                         ? `#9c9396  ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
-                        : props?.ContainingCollectionDoc?._useClusters && doc.type !== DocumentType.INK
+                        : props?.ContainingCollectionDoc?._useClusters
                         ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
-                        : NumCast(doc.group, -1) !== -1 && doc.type !== DocumentType.INK
+                        : NumCast(doc.group, -1) !== -1
                         ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
                         : isBackground()
                         ? undefined // if it's a background & has a cluster color, make the shadow spread really big
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a28b1ca19..90406ed3c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -256,7 +256,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle);
     childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
     @computed get childLayoutString() {
-        return StrCast(this.rootDoc.childLayoutString);
+        return StrCast(this.rootDoc.childLayoutString, this.props.childLayoutString);
     }
 
     isContentActive = (outsideReaction?: boolean) => this.props.isContentActive();
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 25fccd89c..bf8d449ea 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -274,7 +274,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 pinDoc.presStartTime = NumCast(doc.clipStart);
                 pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
             }
-            PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, pinDoc);
+            PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, DocCast(pinDoc.proto));
             pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
             Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
             //save position
@@ -359,7 +359,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         // prettier-ignore
         switch (whereFields[0]) {
             case OpenWhere.inPlace:  // fall through to lightbox
-            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
+            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
             case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
@@ -382,14 +382,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     };
     @action
     focusFunc = (doc: Doc, options: DocFocusOptions) => {
-        const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap;
-        if (options?.willPanZoom !== false && shrinkwrap && this._document) {
-            const focusSpeed = options.zoomTime ?? 500;
-            shrinkwrap();
-            this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false));
-        } else {
-            options?.afterFocus?.(false);
-        }
+        options?.afterFocus?.(false);
+
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
             this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index bd326f917..2398d8f58 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -410,7 +410,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 if (aspect) return this.embeddedPanelWidth() / (aspect || 1);
                 return layoutDoc._fitWidth
                     ? !Doc.NativeHeight(layoutDoc)
-                        ? NumCast(layoutDoc._height) //this.props.containerCollection._height)
+                        ? NumCast(layoutDoc._height)
                         : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height)))
                     : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym]();
             })()
@@ -957,6 +957,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         );
     };
 
+    fitWidthFilter = (doc: Doc) => (doc.type === DocumentType.IMG ? false : undefined);
     renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
         return (
             <div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
@@ -965,6 +966,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                     ref={action((r: DocumentView | null) => (this._dref = r))}
                     Document={this.doc}
                     DataDoc={undefined}
+                    fitWidth={this.fitWidthFilter}
                     PanelWidth={this.embeddedPanelWidth}
                     PanelHeight={this.embeddedPanelHeight}
                     LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 51672578e..d6e95f97f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -16,7 +16,7 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnTransparent, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -61,6 +61,7 @@ export type collectionFreeformViewProps = {
     scaleField?: string;
     noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
     engineProps?: any;
+    getScrollHeight?: () => number | undefined;
     dontScaleFilter?: (doc: Doc) => boolean; // whether this collection should scale documents to fit their panel vs just scrolling them
     dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not.
     // However, this screws up interactions since only the top layer gets events.  so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
@@ -363,10 +364,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const grouping = this.props.Document._useClusters ? NumCast(cd.cluster, -1) : NumCast(cd.group, -1);
                 if (grouping !== -1) {
                     const layoutDoc = Doc.Layout(cd);
-                    const cx = NumCast(cd.x) - this._clusterDistance;
-                    const cy = NumCast(cd.y) - this._clusterDistance;
-                    const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance;
-                    const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance;
+                    const cx = NumCast(cd.x) - this._clusterDistance / 2;
+                    const cy = NumCast(cd.y) - this._clusterDistance / 2;
+                    const cw = NumCast(layoutDoc._width) + this._clusterDistance;
+                    const ch = NumCast(layoutDoc._height) + this._clusterDistance;
                     return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster;
                 }
                 return cluster;
@@ -545,7 +546,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.None:
                         if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false);
                         }
                         break;
                 }
@@ -771,7 +772,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         } else if (!e.cancelBubble) {
             if (this.tryDragCluster(e, this._hitCluster)) {
                 return true;
-            } else this.pan(e);
+            }
+            this.pan(e);
         }
         return false;
     };
@@ -1004,6 +1006,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (deltaScale * invTransform.Scale > 20) {
             deltaScale = 20 / invTransform.Scale;
         }
+        if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
+            return;
+        }
         if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
             deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale;
         }
@@ -1012,29 +1017,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) {
             const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20);
             this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
-            this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+            this.setPan(-localTransform.TranslateX / safeScale, NumCast(this.props.Document.scrollTop) * safeScale || -localTransform.TranslateY / safeScale);
         }
     };
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
+        if (this.Document._isGroup) return; // group style collections neither pan nor zoom
         PresBox.Instance?.pauseAutoPres();
         if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
         e.preventDefault();
         switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) {
             case freeformScrollMode.Pan:
-                // if shift is selected then zoom
+                // if ctrl is selected then zoom
                 if (e.ctrlKey) {
-                    if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
-                        // things that can scroll vertically should do that instead of zooming
-                    } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
+                    if (this.props.isContentActive(true)) {
                         !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?
                     }
-                    // otherwise pan
-                } else if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
-                    // things that can scroll vertically should do that instead of zooming
-                } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
+                } // otherwise pan
+                else if (this.props.isContentActive(true)) {
                     const dx = -e.deltaX;
                     const dy = -e.deltaY;
                     if (e.shiftKey) {
@@ -1046,9 +1048,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 break;
             default:
             case freeformScrollMode.Zoom:
-                if (!e.ctrlKey && (this.props.Document.scrollHeight ?? this.props.DocumentView?.().ComponentView?.getScrollHeight?.()) !== undefined) {
-                    // things that can scroll vertically should do that instead of zooming
-                } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
+                if (this.props.isContentActive(true)) {
                     !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?
                 }
                 break;
@@ -1096,7 +1096,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const minPanX = NumCast(this.rootDoc._panXMin, 0);
             const minPanY = NumCast(this.rootDoc._panYMin, 0);
             const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX));
-            const newPanY = Math.min(this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : minPanY + (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, this.nativeHeight), Math.max(minPanY, panY));
+            const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale;
+            const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight;
+            const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight();
+            const maxPanY =
+                minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by fitWidth
+                (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) +
+                (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
+            let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
+            if (NumCast(this.rootDoc.scrollTop) && NumCast(this.rootDoc._viewScale, minScale) !== minScale) {
+                const relTop = NumCast(this.rootDoc.scrollTop) / maxScrollTop;
+                this.rootDoc.scrollTop = undefined;
+                newPanY = minPanY + relTop * (maxPanY - minPanY);
+            } else if (fitYscroll && this.rootDoc.scrollTop === undefined && NumCast(this.rootDoc._viewScale, minScale) === minScale) {
+                const maxPanY = minPanY + fitYscroll;
+                const relTop = (panY - minPanY) / (maxPanY - minPanY);
+                setTimeout(() => {
+                    this.rootDoc.scrollTop = relTop * maxScrollTop;
+                }, 10);
+                newPanY = minPanY;
+            }
             !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
             !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
         }
@@ -1169,7 +1188,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
         //     SelectionManager.DeselectAll();
         // }
-        if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined || doc.type === DocumentType.MARKER) {
+        if (this.props.getScrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined || doc.type === DocumentType.MARKER) {
             this.props.focus(doc, options);
         } else {
             const xfToCollection = options?.docTransform ?? Transform.Identity();
@@ -2033,10 +2052,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             : (this.props.pointerEvents?.() as any),
                     transform: `scale(${this.nativeDimScaling || 1})`,
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
-                    height:
-                        this.isAnnotationOverlay && (this.Document.scrollHeight || this.props.DocumentView?.().ComponentView?.getScrollHeight?.())
-                            ? NumCast(this.Document.scrollHeight || this.props.DocumentView?.().ComponentView?.getScrollHeight?.())
-                            : `${100 / (this.nativeDimScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
+                    height: this.props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
                 {this._firstRender ? this.placeholder : this.marqueeView}
                 {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 0b7854926..bc3b17cd9 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -431,8 +431,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @undoBatch
     @action
-    collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => {
-        const selected = this.marqueeSelect(false);
+    collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => {
+        const selected = selection ?? this.marqueeSelect(false);
         const activeFrame = selected.reduce((v, d) => v ?? Cast(d._activeFrame, 'number', null), undefined as number | undefined);
         if (e instanceof KeyboardEvent ? 'cg'.includes(e.key) : true) {
             this.props.removeDocument?.(selected);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b94db2c6b..36c0240f1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -56,6 +56,7 @@ import { ScriptingBox } from './ScriptingBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { PinProps } from './trails/PresBox';
 import React = require('react');
+import { GestureOverlay } from '../GestureOverlay';
 const { Howl } = require('howler');
 
 interface Window {
@@ -139,7 +140,6 @@ export interface DocComponentView {
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
-    getScrollHeight?: () => number;
     getCenter?: (xf: Transform) => { X: number; Y: number };
     ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
     ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
@@ -170,7 +170,7 @@ export interface DocumentViewSharedProps {
     dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
     styleProvider: Opt<StyleProviderFunc>;
     focus: DocFocusFunc;
-    fitWidth?: (doc: Doc) => boolean;
+    fitWidth?: (doc: Doc) => boolean | undefined;
     docFilters: () => string[];
     docRangeFilters: () => string[];
     searchFilterDocs: () => Doc[];
@@ -637,8 +637,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                             console.log
                         );
                     UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
-                } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) {
-                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
+                } else if (!Doc.IsSystem(this.rootDoc) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) {
+                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
@@ -669,7 +669,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     this._timeout = setTimeout(() => {
                         this._timeout = undefined;
                         clickFunc();
-                    }, 350);
+                    }, 150);
                 } else clickFunc();
             } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 SelectionManager.DeselectAll();
@@ -698,6 +698,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        if (!(e.nativeEvent as any).DownDocView) (e.nativeEvent as any).DownDocView = GestureOverlay.DownDocView = this.props.DocumentView();
         if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
         // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
         if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) {
@@ -1640,7 +1641,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return this.docView?.LayoutFieldKey || 'layout';
     }
     get fitWidth() {
-        return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth;
+        return this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth;
     }
 
     @computed get hideLinkButton() {
@@ -1688,8 +1689,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     }
     @computed get panelHeight() {
         if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
-            const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0;
-            return Math.min(this.props.PanelHeight(), Math.max(scrollHeight, this.effectiveNativeHeight) * this.nativeScaling);
+            return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling);
         }
         return this.props.PanelHeight();
     }
@@ -1902,6 +1902,11 @@ ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
     documentView.select(false);
 });
 
+ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
+    //documentView.iconify(() =>
+    LightboxView.AddDocTab(documentView.rootDoc, OpenWhere.lightbox, 'layout'); //, 0);
+});
+
 ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
     if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
     else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index bdd99528b..540958941 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -26,13 +26,15 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
 import { FaceRectangles } from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import { PresBox } from './trails';
 import React = require('react');
 import Color = require('color');
+import { LinkDocPreview } from './LinkDocPreview';
+import { DocumentManager } from '../../util/DocumentManager';
 
 export const pageSchema = createSchema({
     googlePhotosUrl: 'string',
@@ -51,6 +53,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
+    private _ignoreScroll = false;
+    private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
@@ -119,6 +123,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             },
             { fireImmediately: true }
         );
+        this._disposers.scroll = reaction(
+            () => this.layoutDoc._scrollTop,
+            s_top => {
+                this._forcedScroll = true;
+                !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top));
+                this._mainCont.current?.scrollTo({ top: NumCast(s_top) });
+                this._forcedScroll = false;
+            },
+            { fireImmediately: true }
+        );
     }
 
     componentWillUnmount() {
@@ -155,6 +169,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @undoBatch
     setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']);
 
+    @undoBatch
+    setNativeSize = action(() => {
+        const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._viewScale, 1);
+        const nscale = NumCast(this.props.PanelWidth()) / scaling;
+        const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
+        const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
+        this.dataDoc[this.fieldKey + '-nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']) * nh;
+        this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nw;
+        this.rootDoc._panX = nh * NumCast(this.rootDoc._panX);
+        this.rootDoc._panY = nw * NumCast(this.rootDoc._panY);
+        this.dataDoc._panXMax = this.dataDoc._panXMax ? nh * NumCast(this.dataDoc._panXMax) : undefined;
+        this.dataDoc._panXMin = this.dataDoc._panXMin ? nh * NumCast(this.dataDoc._panXMin) : undefined;
+        this.dataDoc._panYMax = this.dataDoc._panYMax ? nw * NumCast(this.dataDoc._panYMax) : undefined;
+        this.dataDoc._panYMin = this.dataDoc._panYMin ? nw * NumCast(this.dataDoc._panYMin) : undefined;
+    });
     @undoBatch
     rotate = action(() => {
         const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
@@ -189,6 +218,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const croppingProto = Doc.GetProto(cropping);
         croppingProto.annotationOn = undefined;
         croppingProto.isPrototype = true;
+        croppingProto.backgroundColor = undefined;
         croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
         croppingProto.type = DocumentType.IMG;
         croppingProto.layout = ImageBox.LayoutString('data');
@@ -205,7 +235,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.panYMax = anchh / viewScale;
         if (addCrop) {
             DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
+            cropping.y = NumCast(this.rootDoc.y);
+            this.props.addDocTab(cropping, OpenWhere.inParent);
         }
+        DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
         this.props.bringToFront(cropping);
         return cropping;
     };
@@ -216,6 +250,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             const funcs: ContextMenuProps[] = [];
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' });
+            funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
             funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, event: this.setUseAlt, icon: 'expand-arrows-alt' });
             funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' });
             if (!Doc.noviceMode) {
@@ -282,6 +317,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />;
     };
 
+    getScrollHeight = () => (this.props.fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._viewScale, 1) === NumCast(this.rootDoc._viewScaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+
     @computed
     private get considerDownloadIcon() {
         const data = this.dataDoc[this.fieldKey];
@@ -346,9 +383,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get content() {
         TraceMobx();
 
-        const col = Color(this.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
-        const srcpath = !col.alpha() ? '' : this.paths[0];
-        const fadepath = !col.alpha() ? '' : this.paths.lastElement();
+        const col = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
+        const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
+        const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
         const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
         const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
         const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
@@ -366,7 +403,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         return (
             <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
-                <div className="imageBox-fader" style={{ opacity: col.alpha(), overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}>
+                <div className="imageBox-fader" style={{ opacity: col.alpha() }}>
                     <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                     {fadepath === srcpath ? null : (
                         <div
@@ -383,7 +420,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     }
 
-    screenToLocalTransform = this.props.ScreenToLocalTransform;
     contentFunc = () => [this.content];
 
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -394,6 +430,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
     }
+    screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop) * this.props.ScreenToLocalTransform().Scale);
     marqueeDown = (e: React.PointerEvent) => {
         if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
             setupMoveUpEvents(
@@ -410,7 +447,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             );
         }
     };
-    getScrollHeight = () => this.nativeSize.nativeHeight;
     @action
     finishMarquee = () => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
@@ -418,11 +454,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.props.select(false);
     };
     savedAnnotations = () => this._savedAnnotations;
-    styleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
-        if (property === StyleProp.BoxShadow) return undefined;
-        return this.props.styleProvider?.(doc, props, property);
-    };
-
     render() {
         TraceMobx();
         const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -432,25 +463,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 className="imageBox"
                 onContextMenu={this.specificContextMenu}
                 ref={this._mainCont}
+                onScroll={action(e => {
+                    if (!this._forcedScroll) {
+                        if (this.layoutDoc._scrollTop || this._mainCont.current?.scrollTop) {
+                            this._ignoreScroll = true;
+                            this.layoutDoc._scrollTop = this._mainCont.current?.scrollTop;
+                            this._ignoreScroll = false;
+                        }
+                    }
+                })}
                 style={{
                     width: this.props.PanelWidth() ? undefined : `100%`,
                     height: this.props.PanelWidth() ? undefined : `100%`,
                     pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
                     borderRadius,
-                    overflow: 'auto',
+                    overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined,
                 }}>
                 <CollectionFreeFormView
                     {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
                     renderDepth={this.props.renderDepth + 1}
                     fieldKey={this.annotationKey}
-                    styleProvider={this.styleProvider}
+                    styleProvider={this.props.styleProvider}
                     CollectionView={undefined}
                     isAnnotationOverlay={true}
                     annotationLayerHostsContent={true}
                     PanelWidth={this.props.PanelWidth}
-                    PanelHeight={this.props.fitWidth ? '100%' : this.props.PanelHeight}
+                    PanelHeight={this.props.PanelHeight}
                     ScreenToLocalTransform={this.screenToLocalTransform}
                     select={emptyFunction}
+                    getScrollHeight={this.getScrollHeight}
                     NativeDimScaling={returnOne}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index eba9e281d..1b37dc7ab 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -60,6 +60,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
             } else {
                 this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
                 this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
+                this.rootDoc.linkAutoMove = false;
             }
         }
         return false;
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 95cb49037..5940fc075 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -542,8 +542,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         // }
     };
 
-    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
-    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
+    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
     scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
     opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d0d638e98..c4cca9679 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -295,12 +295,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         if (doc !== this.rootDoc && this._outerRef.current) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
-            const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight()));
+            const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this._scrollHeight));
             if (scrollTo !== undefined && this._initialScroll === undefined) {
                 const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
                 this.goTo(scrollTo, focusSpeed, options.easeFunc);
                 return focusSpeed;
-            } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
+            } else if (!this._webPageHasBeenRendered || !this._scrollHeight || this._initialScroll !== undefined) {
                 this._initialScroll = scrollTo;
             }
         }
@@ -376,9 +376,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             ContextMenu.Instance.setIgnoreEvents(true);
         }
     };
-
-    getScrollHeight = () => this._scrollHeight;
-
     isFirefox = () => {
         return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1;
     };
@@ -969,8 +966,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
     showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
     setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
-    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
-    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth;
+    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
     scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 619c59f0e..80b18b8b9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -994,7 +994,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return this._didScroll ? this._focusSpeed : didToggle ? 1 : undefined; // if we actually scrolled, then return some focusSpeed
     };
 
-    getScrollHeight = () => this.scrollHeight;
     // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
     // Since we also monitor all component height changes, this will update the document's height.
     resetNativeHeight = (scrollHeight: number) => {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 427911bd3..0a96297b7 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -35,6 +35,7 @@ import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
+import { PresElementBox } from './PresElementBox';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -383,9 +384,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>([...DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered), ...DocListCast(activeItem.presAnnotations)]);
         }
         if (pinDataTypes.dataview && activeItem.presData !== undefined) {
+            bestTarget._dataTransition = `all ${transTime}ms`;
             const fkey = Doc.LayoutFieldKey(bestTarget);
             Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
             bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt;
+            setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
         }
         if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
         if (pinDataTypes.poslayoutview) {
@@ -2244,6 +2247,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 ignoreUnrendered={true}
                                 //childFitWidth={returnTrue}
                                 childOpacity={returnOne}
+                                //childLayoutString={PresElementBox.LayoutString('data')}
                                 childLayoutTemplate={this.childLayoutTemplate}
                                 childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
                                 filterAddDocument={this.addDocumentFilter}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index c53cc608c..4a7f5d038 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -11,6 +11,7 @@ import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
 import { LinkPopup } from '../linking/LinkPopup';
 import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
 import './AnchorMenu.scss';
+import { LightboxView } from '../LightboxView';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b0b7816b8..9610a71ac 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -144,6 +144,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
     };
 
+    @observable _scrollHeight = 0;
+
     @action
     initialLoad = async () => {
         if (this._pageSizes.length === 0) {
@@ -164,8 +166,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     )
                 )
             );
-            this.props.Document.scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72;
         }
+        runInAction(() => (this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72));
     };
 
     _scrollStopper: undefined | (() => void);
@@ -177,7 +179,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
-            const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
+            const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, this._scrollHeight);
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
                 else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
@@ -453,10 +455,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
     };
 
-    scrollXf = () => {
-        return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform();
-    };
-
     onClick = (e: React.MouseEvent) => {
         this._scrollStopper?.();
         if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
@@ -504,10 +502,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
         );
     }
 
+    getScrollHeight = () => this._scrollHeight;
     showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
+    scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform());
     overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1));
-    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
-    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1);
+    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
     opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
@@ -533,6 +533,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 isAnnotationOverlay={true}
                 fieldKey={this.props.fieldKey + '-annotations'}
                 CollectionView={undefined}
+                getScrollHeight={this.getScrollHeight}
                 setPreviewCursor={this.setPreviewCursor}
                 setBrushViewer={this.setBrushViewer}
                 PanelHeight={this.panelHeight}
diff --git a/src/fields/util.ts b/src/fields/util.ts
index dc0b41276..3a7484cfd 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -350,7 +350,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any {
                         return target[prop];
         case AclSym :   return target[AclSym];
         case $mobx:     return target.__fields[prop];
-        case LayoutSym: return target.__Layout__;
+        case LayoutSym: return target.__LAYOUT__;
         case HeightSym: case WidthSym: if (GetEffectiveAcl(target) === AclPrivate) return returnZero;
         default :
             if (typeof prop === 'symbol')  return target[prop];
-- 
cgit v1.2.3-70-g09d2


From f303131b40fac4909ab2730b369ef3bdb8e00b1d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 21 Feb 2023 09:53:18 -0500
Subject: fixed explore mode zooming.  don't show doc decorations until you
 move outside of the document, fixed dragging radius button over webbox to
 still get pointer events. fixed selection text in coments for web boxes..
 fixed setting default background color for notes.  fixed mode buttons to
 trigger click behavior before double click behavior.  fixed events on nested
 text boxes that are linkAnchors (like text quotes in sidebar comments),

---
 src/Utils.ts                                       |  9 +++---
 src/client/documents/Documents.ts                  |  5 ++--
 src/client/util/CurrentUserUtils.ts                |  7 +++--
 src/client/views/DocumentDecorations.tsx           | 20 +++++++------
 src/client/views/Main.tsx                          |  5 ++--
 src/client/views/MarqueeAnnotator.tsx              |  2 +-
 src/client/views/SidebarAnnos.tsx                  | 25 ++++++++--------
 src/client/views/StyleProvider.tsx                 |  3 +-
 .../views/collections/CollectionStackingView.tsx   |  8 ++++--
 .../views/collections/CollectionTreeView.tsx       |  5 +++-
 src/client/views/collections/TreeView.tsx          | 33 +++++++++++++++++-----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  6 +++-
 src/client/views/nodes/DocumentView.tsx            |  6 ++--
 src/client/views/nodes/ImageBox.tsx                |  4 +--
 src/client/views/nodes/WebBox.tsx                  | 11 ++++++--
 src/client/views/nodes/button/FontIconBox.tsx      | 15 +++-------
 .../views/nodes/formattedText/DashFieldView.tsx    | 22 +++++++++++++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 22 ++++++---------
 .../views/nodes/formattedText/RichTextMenu.tsx     | 21 ++++++++------
 src/client/views/topbar/TopBar.tsx                 |  2 +-
 20 files changed, 145 insertions(+), 86 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 9d3b9eb2b..22c8cb902 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -686,8 +686,9 @@ export function DashColor(color: string) {
     try {
         return color ? Color(color.toLowerCase()) : Color('transparent');
     } catch (e) {
-        console.log('COLOR error:', e);
-        return Color('red');
+        if (color.includes('gradient')) console.log("using color 'white' in place of :" + color);
+        else console.log('COLOR error:', e);
+        return Color('white');
     }
 }
 
@@ -816,7 +817,7 @@ export function setupMoveUpEvents(
             (target as any)._noClick = clickEvent(e, (target as any)._doubleTap);
         }
         document.removeEventListener('pointermove', _moveEvent);
-        document.removeEventListener('pointerup', _upEvent);
+        document.removeEventListener('pointerup', _upEvent, true);
     };
     const _clickEvent = (e: MouseEvent): void => {
         if ((target as any)._noClick) e.stopPropagation();
@@ -827,6 +828,6 @@ export function setupMoveUpEvents(
         e.preventDefault();
     }
     document.addEventListener('pointermove', _moveEvent);
-    document.addEventListener('pointerup', _upEvent);
+    document.addEventListener('pointerup', _upEvent, true);
     document.addEventListener('click', _clickEvent, true);
 }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3faa6e11d..debb11066 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -224,6 +224,7 @@ export class DocumentOptions {
     contextMenuScripts?: List<ScriptField>;
     contextMenuLabels?: List<string>;
     contextMenuIcons?: List<string>;
+    allowClickBeforeDoubleClick?: boolean; // whether a click function can fire before the timeout for a double click has expired
     dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
     description?: string; // added for links
     layout?: string | Doc; // default layout string for a document
@@ -583,7 +584,7 @@ export namespace Docs {
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: defaultDataKey },
-                    options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
+                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
                 },
             ],
             [
@@ -1808,7 +1809,7 @@ export namespace DocUtils {
             _yMargin: noMargins ? 0 : undefined,
             annotationOn,
             docMaxAutoHeight: maxHeight,
-            backgroundColor: backgroundColor,
+            backgroundColor,
             _width: width || 200,
             _height: 35,
             x: x,
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f678c8936..3d95cb947 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -612,6 +612,7 @@ export class CurrentUserUtils {
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
             { title: "Size",    toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton,               ignoreClick: true, scripts: {script:  '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
             { title: "Color",   toolTip: "Font color",           btnType: ButtonType.ColorButton,  icon: "font", ignoreClick: true, scripts: {script:  '{ return setFontColor(value, _readOnly_);}'}},
+            { title: "Highlight",toolTip:"Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", ignoreClick: true, scripts: {script:  '{ return setFontHighlight(value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
             { title: "Bold",    toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",                    scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
             { title: "Italic",  toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",                  scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
             { title: "Under",   toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",               scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} },
@@ -635,9 +636,9 @@ export class CurrentUserUtils {
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
             // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
-            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",           scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",           scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",            scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                  btnType: ButtonType.ToggleButton, icon: "user-circle",                 scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
             { title: "Fill",   toolTip: "Fill color",            btnType: ButtonType.ColorButton,  icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
             { title: "Width",  toolTip: "Stroke width",          btnType: ButtonType.NumberButton,                   ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 41f4a17fb..21f63ada4 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { IconButton } from 'browndash-components';
-import { action, computed, observable, reaction } from 'mobx';
+import { action, computed, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { FaUndo } from 'react-icons/fa';
 import { DateField } from '../../fields/DateField';
@@ -182,7 +182,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         dragData.removeDocument = dragDocView.props.removeDocument;
         dragData.isDocDecorationMove = true;
         dragData.canEmbed = dragTitle;
-        this._hidden = this.Interacting = true;
+        this._hidden = true;
         DragManager.StartDocumentDrag(
             SelectionManager.Views().map(dv => dv.ContentDiv!),
             dragData,
@@ -191,7 +191,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             {
                 dragComplete: action(e => {
                     dragData.canEmbed && SelectionManager.DeselectAll();
-                    this._hidden = this.Interacting = false;
+                    this._hidden = false;
                 }),
                 hideSource: true,
             }
@@ -293,7 +293,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
      */
     @action
     onRadiusDown = (e: React.PointerEvent): void => {
-        this._isRounding = true;
+        this._isRounding = DocumentDecorations.Instance.Interacting = true;
         this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
         // Call util move event function
         setupMoveUpEvents(
@@ -316,10 +316,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 return false;
             }, // moveEvent
             action(e => {
-                this._isRounding = false;
+                DocumentDecorations.Instance.Interacting = this._isRounding = false;
                 this._resizeUndo?.end();
             }), // upEvent
-            e => {} // clickEvent
+            e => {}, // clickEvent,
+            true
         );
     };
 
@@ -710,11 +711,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         return this._rotCenter;
     }
 
+    @observable _showNothing = true;
+
     render() {
         const { b, r, x, y } = this.Bounds;
         const bounds = { b, r, x, y };
-        const seldocview = SelectionManager.Views().slice(-1)[0];
+        const seldocview = SelectionManager.Views().lastElement();
         if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+            setTimeout(action(() => (this._showNothing = true)));
             return null;
         }
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
@@ -812,7 +816,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             </div>
         );
         return (
-            <div className={`documentDecorations${colorScheme}`}>
+            <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }} onPointerOver={action(e => (this._showNothing = false))}>
                 <div
                     className="documentDecorations-background"
                     style={{
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index e0b4c5159..6b18caed0 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -22,7 +22,8 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
 
 (async () => {
     MainView.Live = window.location.search.includes('live');
-    ReactDOM.createRoot(document.getElementById('root')!).render(<FieldLoader />);
+    const root = ReactDOM.createRoot(document.getElementById('root')!);
+    root.render(<FieldLoader />);
     window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
     const info = await CurrentUserUtils.loadCurrentUser();
     if (info.email === 'guest') DocServer.Control.makeReadOnly();
@@ -47,6 +48,6 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
         new LinkManager();
         new TrackMovements();
         new ReplayMovements();
-        ReactDOM.createRoot(document.getElementById('root')!).render(<MainView />);
+        root.render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 5ab91dd70..3bdf65d01 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -49,7 +49,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         super(props);
 
         AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true);
-        AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true));
+        AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true));
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = this.highlight;
         AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 6d06bbbf6..b9af28413 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -88,6 +88,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 };
             });
 
+        if (!anchor.text) Doc.GetProto(anchor).text = '-selection-';
         const textLines: any = [
             {
                 type: 'paragraph',
@@ -121,16 +122,18 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             content: taggedContent,
         };
         if (taggedContent.length) textLines.push(metadatatext);
-        Doc.GetProto(target).text = new RichTextField(
-            JSON.stringify({
-                doc: {
-                    type: 'doc',
-                    content: textLines,
-                },
-                selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
-            }),
-            ''
-        );
+        if (textLines.length) {
+            Doc.GetProto(target).text = new RichTextField(
+                JSON.stringify({
+                    doc: {
+                        type: 'doc',
+                        content: textLines,
+                    },
+                    selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
+                }),
+                ''
+            );
+        }
         this.addDocument(target);
         setTimeout(() => this._stackRef.current?.focusDocument(target, {}));
         return target;
@@ -233,7 +236,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                         isAnnotationOverlay={false}
                         select={emptyFunction}
                         NativeDimScaling={returnOne}
-                        childShowTitle={this.showTitle}
+                        //childShowTitle={this.showTitle}
                         childDocumentsActive={this.props.isContentActive}
                         whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                         childHideDecorationTitle={returnTrue}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3cb920ba0..817baeae6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -19,6 +19,7 @@ import { SliderBox } from './nodes/SliderBox';
 import './StyleProvider.scss';
 import React = require('react');
 import { Shadows } from 'browndash-components';
+import { SelectionManager } from '../util/SelectionManager';
 
 export enum StyleProp {
     TreeViewIcon = 'treeViewIcon',
@@ -116,7 +117,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                 const excludeTypes = [DocumentType.FONTICON];
                 let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents.  need to flag a document as not highlightable in a more direct way
                 if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') {
-                    return { highlightStyle, highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK };
+                    return { highlightStyle, highlightColor: SelectionManager.Views().some(dv => dv.rootDoc === doc) ? 'black' : highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK };
                 }
             }
             return undefined;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 2f495d55c..6314b4529 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -129,6 +129,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         TraceMobx();
         // appears that we are going to reset the _docXfs. TODO: what is Xfs?
         this._docXfs.length = 0;
+        this._renderCount < docs.length && setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5))));
         return docs.map((d, i) => {
             const height = () => this.getDocHeight(d);
             const width = () => this.getDocWidth(d);
@@ -139,7 +140,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
                 <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
-                    {this.getDisplayDoc(d, width)}
+                    {this.getDisplayDoc(d, width, i)}
                 </div>
             );
         });
@@ -297,12 +298,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     };
     isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined);
 
+    @observable _renderCount = 5;
     isChildContentActive = () =>
         this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
     isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined);
     // this is what renders the document that you see on the screen
     // called in Children: this actually adds a document to our children list
-    getDisplayDoc(doc: Doc, width: () => number) {
+    getDisplayDoc(doc: Doc, width: () => number, count: number) {
         const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
         const height = () => this.getDocHeight(doc);
 
@@ -310,7 +312,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         const stackedDocTransform = () => this.getDocTransform(doc, dref);
         this._docXfs.push({ stackedDocTransform, width, height });
         //DocumentView is how the node will be rendered
-        return (
+        return count > this._renderCount ? null : (
             <DocumentView
                 ref={r => (dref = r || undefined)}
                 Document={doc}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 1a265af4a..456f2a13d 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -259,11 +259,13 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
     };
     headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields);
+    @observable _renderCount = 1;
     @computed get treeViewElements() {
         TraceMobx();
         const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
         const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
         const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false;
+        if (this.treeChildren.length < this._renderCount) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
         return TreeView.GetChildElements(
             this.treeChildren,
             this,
@@ -296,7 +298,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             //TODO: [AL] add these
             this.props.AddToMap,
             this.props.RemFromMap,
-            this.props.hierarchyIndex
+            this.props.hierarchyIndex,
+            this._renderCount
         );
     }
     @computed get titleBar() {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 2398d8f58..0d2968ba1 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -32,6 +32,7 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
 import { CollectionView } from './CollectionView';
 import './TreeView.scss';
 import React = require('react');
+import { render } from 'react-dom';
 
 export interface TreeViewProps {
     treeView: CollectionTreeView;
@@ -95,7 +96,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     private _header: React.RefObject<HTMLDivElement> = React.createRef();
     private _tref = React.createRef<HTMLDivElement>();
     @observable _docRef: Opt<DocumentView>;
-    private _selDisposer: Opt<IReactionDisposer>;
+    private _disposers: { [name: string]: IReactionDisposer } = {};
     private _editTitleScript: (() => ScriptField) | undefined;
     private _openScript: (() => ScriptField) | undefined;
     private _treedropDisposer?: DragManager.DragDropDisposer;
@@ -212,14 +213,14 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
 
     @action setEditTitle = (docView?: DocumentView) => {
-        this._selDisposer?.();
+        this._disposers.selection?.();
         if (!docView) {
             this._editTitle = false;
         } else if (docView.isSelected()) {
             const doc = docView.Document;
             SelectionManager.SelectSchemaViewDoc(doc);
             this._editTitle = true;
-            this._selDisposer = reaction(
+            this._disposers.selection = reaction(
                 () => SelectionManager.SelectedSchemaDoc(),
                 seldoc => seldoc !== doc && this.setEditTitle(undefined)
             );
@@ -259,7 +260,8 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
 
     componentWillUnmount() {
-        this._selDisposer?.();
+        this._renderTimer && clearTimeout(this._renderTimer);
+        Object.values(this._disposers).forEach(disposer => disposer?.());
         this._treeEle && this.props.unobserveHeight(this._treeEle);
         document.removeEventListener('pointermove', this.onDragMove, true);
         document.removeEventListener('pointermove', this.onDragUp, true);
@@ -268,6 +270,10 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
 
     componentDidUpdate() {
+        this._disposers.opening = reaction(
+            () => this.treeViewOpen,
+            open => !open && (this._renderCount = 20)
+        );
         this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
     }
 
@@ -512,6 +518,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         return rows;
     }
 
+    _renderTimer: any;
+    @observable _renderCount = 1;
     @computed get renderContent() {
         TraceMobx();
         const expandKey = this.treeViewExpandedView;
@@ -543,6 +551,14 @@ export class TreeView extends React.Component<TreeViewProps> {
             const docs = expandKey === 'aliases' ? this.childAliases : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
             let downX = 0,
                 downY = 0;
+            if (docs?.length && this._renderCount < docs?.length) {
+                this._renderTimer && clearTimeout(this._renderTimer);
+                this._renderTimer = setTimeout(
+                    action(() => {
+                        this._renderCount = Math.min(docs!.length, this._renderCount + 20);
+                    })
+                );
+            }
             return (
                 <>
                     {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : (
@@ -599,7 +615,8 @@ export class TreeView extends React.Component<TreeViewProps> {
                                   // TODO: [AL] add these
                                   this.props.AddToMap,
                                   this.props.RemFromMap,
-                                  this.props.hierarchyIndex
+                                  this.props.hierarchyIndex,
+                                  this._renderCount
                               )}
                     </ul>
                 </>
@@ -651,7 +668,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         return (
             <div
                 className={`bullet${this.props.treeView.outlineMode ? '-outline' : ''}`}
-                key={'bullet'}
+                key="bullet"
                 title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : 'view fields'}
                 onClick={this.bulletClick}
                 style={
@@ -1131,7 +1148,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         // TODO: [AL] add these
         AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
         RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
-        hierarchyIndex?: number[]
+        hierarchyIndex?: number[],
+        renderCount?: number
     ) {
         const viewSpecScript = Cast(containerCollection.viewSpecScript, ScriptField);
         if (viewSpecScript) {
@@ -1144,6 +1162,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         return docs
             .filter(child => child instanceof Doc)
             .map((child, i) => {
+                if (renderCount && i > renderCount) return null;
                 const pair = Doc.GetLayoutDataDocPair(containerCollection, dataDoc, child);
                 if (!pair.layout || pair.data instanceof Promise) {
                     return null;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d6e95f97f..4d6e0dff2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -53,6 +53,7 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import React = require('react');
+import { DocumentDecorations } from '../../DocumentDecorations';
 
 export type collectionFreeformViewProps = {
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -1305,7 +1306,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     pointerEvents = () => {
         const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
         const pointerEvents =
-            this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+            this.props.isContentActive() === false || DocumentDecorations.Instance.Interacting
+                ? 'none'
+                : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
         return pointerEvents;
     };
     getChildDocView(entry: PoolData) {
@@ -2287,6 +2290,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
     SelectionManager.DeselectAll();
     dv.props.focus(dv.props.Document, {
         willPanZoom: true,
+        zoomScale: 0.8,
         afterFocus: async didMove => {
             if (!didMove) {
                 const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 36c0240f1..3d89566ee 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -664,7 +664,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         ? this.props.select(false)
                         : '';
                 const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
-                if (this.onDoubleClickHandler) {
+                if (this.onDoubleClickHandler && !this.props.Document.allowClickBeforeDoubleClick) {
                     runInAction(() => (this._pendingDoubleClick = true));
                     this._timeout = setTimeout(() => {
                         this._timeout = undefined;
@@ -780,7 +780,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
         } else {
-            this._doubleTap = Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
+            const now = Date.now();
+            console.log(now - this._lastTap);
+            this._doubleTap = now - this._lastTap < 600 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
             // bcz: this is a placeholder.  documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
             if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 540958941..c66f2a67b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -383,7 +383,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get content() {
         TraceMobx();
 
-        const col = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
+        const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha();
         const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
         const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
         const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
@@ -403,7 +403,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         return (
             <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
-                <div className="imageBox-fader" style={{ opacity: col.alpha() }}>
+                <div className="imageBox-fader" style={{ opacity: backAlpha }}>
                     <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                     {fadepath === srcpath ? null : (
                         <div
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c392f3577..5bbe521a7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -257,6 +257,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         // this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
     }
 
+    private _selectionText: string = '';
+    private _selectionContent: DocumentFragment | undefined;
+    selectionText = () => this._selectionText;
+    selectionContent = () => this._selectionContent;
     @action
     createTextAnnotation = (sel: Selection, selRange: Range | undefined) => {
         if (this._mainCont.current && selRange) {
@@ -276,6 +280,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             }
         }
         //this._selectionText = selRange.cloneContents().textContent || "";
+        this._selectionContent = selRange?.cloneContents();
+        this._selectionText = this._selectionContent?.textContent || '';
 
         // clear selection
         if (sel.empty) sel.empty(); // Chrome
@@ -854,8 +860,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
     };
     @computed get content() {
-        const interactive =
-            !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
+        const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
         return (
             <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
                 {this.urlContent}
@@ -1014,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 docView={this.props.docViewPath().lastElement()}
                                 finishMarquee={this.finishMarquee}
                                 savedAnnotations={this.savedAnnotationsCreator}
-                                selectionText={returnEmptyString}
+                                selectionText={this.selectionText}
                                 annotationLayer={this._annotationLayer.current}
                                 mainCont={this._mainCont.current}
                             />
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 9d67283a0..b352f3790 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -136,7 +136,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ value, _readOnly_: false }), 'set num value');
 
         // Script for checking the outcome of the toggle
-        const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0;
+        const checkResult = Number(numScript?.script.run({ value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
 
         const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
 
@@ -150,7 +150,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         max={NumCast(this.rootDoc.numBtnMax, 100)}
                         value={checkResult}
                         className={'menu-slider'}
-                        id="slider"
                         onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
                         onPointerUp={() => this._batch?.end()}
                         onChange={e => {
@@ -642,13 +641,7 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo
     if (checkResult) {
         return (selected ?? Doc.UserDoc())._fontHighlight;
     }
-    if (selected) {
-        selected._fontColor = color;
-        if (color) {
-            editorView?.state && RichTextMenu.Instance.setHighlight(color, editorView, editorView?.dispatch);
-        }
-    }
-    Doc.UserDoc()._fontHighlight = color;
+    color && RichTextMenu.Instance.setHighlight(color);
 });
 
 // toggle: Set overlay status of selected document
@@ -795,7 +788,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
             GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
         }
         if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
-            if (GestureOverlay.Instance.InkShape === tool) {
+            if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
                 GestureOverlay.Instance.InkShape = undefined;
             } else {
@@ -804,7 +797,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
             }
         } else if (tool) {
             // pen or eraser
-            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
             } else {
                 Doc.ActiveTool = tool as any;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 39005a18b..b33529aeb 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -18,12 +18,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { NodeSelection } from 'prosemirror-state';
 import { OpenWhere } from '../DocumentView';
+import { FormattedTextBoxComment } from './FormattedTextBoxComment';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
     root: any;
+    node: any;
+    tbox: FormattedTextBox;
 
+    unclickable = () => !this.tbox.props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        this.node = node;
+        this.tbox = tbox;
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
         this.dom.style.height = node.attrs.height;
@@ -44,7 +50,18 @@ export class DashFieldView {
 
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
-            <DashFieldViewInternal node={node} getPos={getPos} fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />
+            <DashFieldViewInternal
+                node={node}
+                unclickable={this.unclickable}
+                getPos={getPos}
+                fieldKey={node.attrs.fieldKey}
+                docid={node.attrs.docid}
+                width={node.attrs.width}
+                height={node.attrs.height}
+                hideKey={node.attrs.hideKey}
+                editable={node.attrs.editable}
+                tbox={tbox}
+            />
         );
     }
     destroy() {
@@ -68,6 +85,7 @@ interface IDashFieldViewInternal {
     editable: boolean;
     node: any;
     getPos: any;
+    unclickable: () => boolean;
 }
 
 @observer
@@ -125,7 +143,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
                 return (
                     <span
                         className="dashFieldView-fieldSpan"
-                        contentEditable={true}
+                        contentEditable={!this.props.unclickable()}
                         style={{ display: strVal.length < 2 ? 'inline-block' : undefined }}
                         suppressContentEditableWarning={true}
                         defaultValue={strVal}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 49acd8a57..0ff09a0ca 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -81,7 +81,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     static _highlightStyleSheet: any = addStyleSheet();
     static _bulletStyleSheet: any = addStyleSheet();
     static _userStyleSheet: any = addStyleSheet();
-    static _canAnnotate = true;
     static _hadSelection: boolean = false;
     private _sidebarRef = React.createRef<SidebarAnnos>();
     private _ref: React.RefObject<HTMLDivElement> = React.createRef();
@@ -102,7 +101,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     private _rules: RichTextRules | undefined;
     private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason.  This flags the 2nd click as happening on a selection which allows bullet points to toggle
     private _forceDownNode: Node | undefined;
-    private _downEvent: any;
     private _downX = 0;
     private _downY = 0;
     private _break = true;
@@ -118,7 +116,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     }
 
     @computed get noSidebar() {
-        return this.props.docViewPath?.()[this.props.docViewPath().length - 2]?.rootDoc.type === DocumentType.RTF || this.props.noSidebar || this.Document._noSidebar;
+        return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._noSidebar;
     }
     @computed get sidebarWidthPercent() {
         return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
@@ -254,7 +252,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             });
         };
         AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
-            this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
+            this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
             return undefined;
         });
         AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
@@ -788,7 +786,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         );
 
         const uicontrols: ContextMenuProps[] = [];
-        !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' });
         uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' });
         uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
         !Doc.noviceMode &&
@@ -1455,7 +1452,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
         this._downX = e.clientX;
         this._downY = e.clientY;
-        this._downEvent = true;
         FormattedTextBoxComment.textBox = this;
         if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) {
             if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
@@ -1477,17 +1473,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         document.removeEventListener('pointermove', this.onSelectMove);
     };
     onPointerUp = (e: React.PointerEvent): void => {
-        if (!this._editorView?.state.selection.empty && !(this._editorView?.state.selection instanceof NodeSelection) && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
-        if (!this._downEvent) return;
-        this._downEvent = false;
-        if (this._editorView?.state.selection.empty && this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
-            const editor = this._editorView!;
+        const editor = this._editorView!;
+        const state = editor?.state;
+        if (!state || !editor) return;
+        if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
+        else if (this.props.isContentActive(true)) {
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
-            !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
+            !this.props.isSelected(true) && editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
             FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
-            if (pcords && pcords.inside > 0 && this._editorView.state.doc.nodeAt(pcords.inside)?.type === this._editorView.state.schema.nodes.dashDoc) {
+            if (pcords && pcords.inside > 0 && state.doc.nodeAt(pcords.inside)?.type === state.schema.nodes.dashDoc) {
                 return;
             }
         }
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index b70da2e5e..f0caa1f4f 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -54,7 +54,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @observable private _activeFontColor: string = 'black';
     @observable private showColorDropdown: boolean = false;
 
-    @observable private activeHighlightColor: string = 'transparent';
+    @observable private _activeHighlightColor: string = 'transparent';
     @observable private showHighlightDropdown: boolean = false;
 
     @observable private currentLink: string | undefined = '';
@@ -89,6 +89,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @computed get fontColor() {
         return this._activeFontColor;
     }
+    @computed get fontHighlight() {
+        return this._activeHighlightColor;
+    }
     @computed get fontFamily() {
         return this._activeFontFamily;
     }
@@ -138,7 +141,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
         this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
         this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
-        this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
+        this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
 
         // update link in current selection
         this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle));
@@ -356,11 +359,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.updateMenu(this.view, undefined, this.props);
     };
 
-    setHighlight(color: String, view: EditorView, dispatch: any) {
-        const highlightMark = view.state.schema.mark(view.state.schema.marks.marker, { highlight: color });
-        if (view.state.selection.empty) return false;
-        view.focus();
-        this.setMark(highlightMark, view.state, dispatch, false);
+    setHighlight(color: string) {
+        if (this.view) {
+            const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color });
+            this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true);
+            this.view.focus();
+        } else Doc.UserDoc()._fontHighlight = color;
+        this.updateMenu(this.view, undefined, this.props);
     }
 
     setColor(color: string) {
@@ -563,7 +568,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     @action setActiveHighlight(color: string) {
-        this.activeHighlightColor = color;
+        this._activeHighlightColor = color;
     }
 
     @action setCurrentLink(link: string) {
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 7e728306c..f2e9be61d 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -54,7 +54,7 @@ export class TopBar extends React.Component {
                         <span style={{ color: Colors.LIGHT_BLUE, fontWeight: 500 }}>dash</span>
                     </div>
                 )}
-                {Doc.ActiveDashboard && !Doc.noviceMode && (
+                {Doc.ActiveDashboard && (
                     <Button
                         text="Explore"
                         tooltip="Browsing mode for directly navigating to documents"
-- 
cgit v1.2.3-70-g09d2


From 99b63e35ddf40fc1a69d50059b72275ce23ac43e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 21 Feb 2023 10:04:34 -0500
Subject: from last - fixes to showing doc decorations when pointer leaves
 selection box.

---
 src/client/views/DocumentDecorations.tsx | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 21f63ada4..c3ef7570b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -66,7 +66,19 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         DocumentDecorations.Instance = this;
         reaction(
             () => SelectionManager.Views().slice(),
-            action(docs => (this._editingTitle = false))
+            action(docs => {
+                docs.length > 1 && (this._showNothing = false); // show decorations if multiple docs are selected
+                this._editingTitle = false;
+            })
+        );
+        document.addEventListener(
+            // show decorations whenever pointer moves outside of selection bounds.
+            'pointermove',
+            action(e => {
+                if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) {
+                    this._showNothing = false;
+                }
+            })
         );
     }
 
@@ -816,7 +828,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             </div>
         );
         return (
-            <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }} onPointerOver={action(e => (this._showNothing = false))}>
+            <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}>
                 <div
                     className="documentDecorations-background"
                     style={{
-- 
cgit v1.2.3-70-g09d2


From ac8a292b2ac077cb530b926824594efab2228a16 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 22 Feb 2023 21:06:34 -0500
Subject: fixed hiding doc decorations when clicking on a nested doc.  finished
 removing old view spec code in favor of saving and restoring anchor data.

---
 src/client/views/DocumentDecorations.tsx           |  1 +
 .../views/collections/CollectionTimeView.tsx       | 75 +++++++++++++---------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 26 ++------
 src/client/views/nodes/DocumentView.tsx            |  9 +--
 src/client/views/nodes/PDFBox.tsx                  |  7 +-
 src/client/views/nodes/WebBox.tsx                  |  4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  7 +-
 src/client/views/nodes/trails/PresBox.tsx          | 25 +++++++-
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 9 files changed, 87 insertions(+), 69 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index c3ef7570b..4e51f10a8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -67,6 +67,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         reaction(
             () => SelectionManager.Views().slice(),
             action(docs => {
+                this._showNothing = true;
                 docs.length > 1 && (this._showNothing = false); // show decorations if multiple docs are selected
                 this._editingTitle = false;
             })
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 8ec683e6d..e6f29ec37 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -7,7 +7,7 @@ import { ObjectField } from '../../../fields/ObjectField';
 import { RichTextField } from '../../../fields/RichTextField';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -21,6 +21,8 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
 import { CollectionSubView } from './CollectionSubView';
 import './CollectionTimeView.scss';
 import React = require('react');
+import { DocFocusOptions } from '../nodes/DocumentView';
+import { PresBox } from '../nodes/trails';
 
 @observer
 export class CollectionTimeView extends CollectionSubView() {
@@ -31,17 +33,27 @@ export class CollectionTimeView extends CollectionSubView() {
     @observable _viewDefDivClick: Opt<ScriptField>;
     @observable _focusPivotField: Opt<string>;
 
+    async componentDidMount() {
+        this.props.setContentView?.(this);
+        //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
+        ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; useRightSplit(alias, shiftKey); ";
+        runInAction(() => {
+            this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
+            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
+        });
+    }
+
+    get pivotField() {
+        return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
+    }
+
     getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.HTMLAnchorDocument([], {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
             annotationOn: this.rootDoc,
         });
-
-        // save view spec information for anchor
-        const proto = Doc.GetProto(anchor);
-        proto.pivotField = this.pivotField;
-        proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]);
-        proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]);
+        anchor.presPinPivotField = this.pivotField; // should be captured in pinDocView below
+        PresBox.pinDocView(anchor, { pinData: { viewType: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
@@ -54,34 +66,33 @@ export class CollectionTimeView extends CollectionSubView() {
         return anchor;
     };
 
-    async componentDidMount() {
-        this.props.setContentView?.(this);
-        //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
-        ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; useRightSplit(alias, shiftKey); ";
-        runInAction(() => {
-            this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
-            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
-        });
-    }
-
-    get pivotField() {
-        return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
-    }
     @action
-    setViewSpec = (anchor: Doc, preview: boolean) => {
-        if (preview) {
+    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
+        if (options.preview) {
             // if in preview, then override document's fields with view spec
-            this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
-            this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
-            this._focusPivotField = StrCast(anchor.pivotField);
-        } else if (anchor.pivotField !== undefined) {
-            // otherwise set document's fields based on anchor view spec
-            this.layoutDoc._prevFilterIndex = 1;
-            this.layoutDoc._pivotField = StrCast(anchor.pivotField);
-            this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
-            this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
+            this._focusFilters = StrListCast(anchor.presPinDocFilters);
+            this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
+            this._focusPivotField = StrCast(anchor.presPinPivotField);
+            return undefined;
         }
-        return 0;
+        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
+
+        // should be part of restoreTargetDocView
+        this.layoutDoc._prevFilterIndex = 1;
+        this.layoutDoc._pivotField = anchor.presPinPivotField;
+
+        return PresBox.restoreTargetDocView(
+            this.props.DocumentView?.(), //
+            { pinDocLayout: BoolCast(anchor.presPinLayout) },
+            anchor,
+            focusSpeed,
+            {
+                viewType: anchor.presPinViewType ? true : false,
+                filters: anchor.presPinDocFilters || anchor.presPinDocRangeFilters ? true : false,
+            }
+        )
+            ? focusSpeed
+            : undefined;
     };
 
     layoutEngine = () => this._layoutEngine;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d69e6b810..3300bc387 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1591,25 +1591,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return elements;
     }
 
-    @action
-    setViewSpec = (anchor: Doc, preview: boolean) => {
-        if (preview) {
-            this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
-            this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
-        } else {
-            if (anchor.docFilters) {
-                this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
-            }
-            if (anchor.docRangeFilters) {
-                this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
-            }
-        }
-        return 0;
-    };
-
     @action
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
+        if (options.preview) {
+            this._focusFilters = StrListCast(anchor.presPinDocFilters);
+            this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
+            return undefined;
+        }
         return PresBox.restoreTargetDocView(
             this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinLayout) },
@@ -1618,6 +1607,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             {
                 pannable: anchor.presPinData ? true : false,
                 viewType: anchor.presPinViewType ? true : false,
+                filters: anchor.presPinDocFilters || anchor.presPinDocRangeFilters ? true : false,
             }
         )
             ? focusSpeed
@@ -1631,9 +1621,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type)
         const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), presTransition: 500, annotationOn: this.rootDoc });
-        PresBox.pinDocView(anchor, { pinData: { pannable: true, viewType: true } }, this.rootDoc);
-        // bcz TODO: saving document filters should be part of pinDocView...
-        Doc.GetProto(anchor).docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
+        PresBox.pinDocView(anchor, { pinData: { pannable: true, viewType: true, filters: true } }, this.rootDoc);
 
         if (addAsAnnotation) {
             if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 94eb0b092..0a923bca9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -107,6 +107,7 @@ export interface DocFocusOptions {
     afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
     docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
     instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+    preview?: boolean; // whether changes should be previewed by the componentView or written to the document
     effect?: Doc; // animation effect for focus
     noSelect?: boolean; // whether target should be selected after focusing
     playAudio?: boolean; // whether to play audio annotation on focus
@@ -123,7 +124,6 @@ export interface DocComponentView {
     getAnchor?: (addAsAnnotation: boolean) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
     scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
-    setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
     menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
@@ -585,10 +585,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         LightboxView.SetCookie(StrCast(anchor['cookies-set']));
 
         // Restore viewing specification of target by reading them out of the anchor and applying to the target doc.
-        // after a render the correct _componentView should be created, so after a timeout, call the componentview to update its specific view specs
-        // bcz: TODO: all viewing specs should be restored via the anchor doc in PresBox.restoreTargetDocView called from scrollFocus() below.
-        setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
-        const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false });
+        const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, preview: LinkDocPreview.LinkInfo ? true : false });
 
         // FOCUS: navigate through the display hierarchy making sure the target is in view
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
@@ -782,7 +779,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
         } else if (now - this._downTime < 300) {
-            this._doubleTap = now - this._lastTap < 600 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
+            this._doubleTap = now - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
             // bcz: this is a placeholder.  documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
             if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5890c536d..88d134bba 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -198,13 +198,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         );
     }
 
-    brushView = (view: { width: number; height: number; panX: number; panY: number }) => {
-        this._pdfViewer?.brushView(view);
-    };
+    brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
+
     scrollFocus = (doc: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
-            this.toggleSidebar(!options.instant);
+            this.toggleSidebar(options.preview);
             didToggle = true;
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 5bbe521a7..4c9bead9d 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -294,9 +294,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
     scrollFocus = (doc: Doc, options: DocFocusOptions) => {
-        if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant);
+        if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.preview);
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
-            this.toggleSidebar(options.instant);
+            this.toggleSidebar(options.preview);
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         if (doc !== this.rootDoc && this._outerRef.current) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0ff09a0ca..788de7af9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -307,7 +307,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 let unchanged = true;
                 if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
                     this._applyingChange = this.fieldKey;
-                    curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text && (dataDoc[this.fieldKey + '-lastModified'] = new DateField(new Date(Date.now())));
+                    const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text;
+                    textChange && (dataDoc[this.fieldKey + '-lastModified'] = new DateField(new Date(Date.now())));
                     if ((!curTemp && !curProto) || curText || json.includes('dash')) {
                         // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
                         if (removeSelection(json) !== removeSelection(curLayout?.Data)) {
@@ -318,7 +319,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                                 dataDoc[this.fieldKey] = new RichTextField(json, curText);
                             }
                             dataDoc[this.fieldKey + '-noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
-                            ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+                            textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
                             unchanged = false;
                         }
                     } else {
@@ -934,7 +935,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
-            this.toggleSidebar(options.instant);
+            this.toggleSidebar(options.preview);
             didToggle = true;
         }
         const textAnchorId = textAnchor[Id];
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 32fcc0fae..4abdaeeb4 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -49,6 +49,7 @@ export interface PinProps {
         scrollable?: boolean | undefined;
         pannable?: boolean | undefined;
         viewType?: boolean | undefined;
+        filters?: boolean | undefined;
         temporal?: boolean | undefined;
         clippable?: boolean | undefined;
         dataview?: boolean | undefined;
@@ -317,7 +318,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; viewType?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
+    static pinDataTypes(target?: Doc): {
+        scrollable?: boolean;
+        pannable?: boolean;
+        viewType?: boolean;
+        filters?: boolean;
+        temporal?: boolean;
+        clippable?: boolean;
+        dataview?: boolean;
+        textview?: boolean;
+        poslayoutview?: boolean;
+        dataannos?: boolean;
+    } {
         const targetType = target?.type as any;
         const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
         const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
@@ -327,8 +339,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
         const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const viewType = targetType === DocumentType.COL;
+        const filters = true;
         const dataannos = false;
-        return { scrollable, pannable, viewType, temporal, clippable, dataview, textview, poslayoutview, dataannos };
+        return { scrollable, pannable, viewType, filters, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
 
     @action
@@ -375,6 +388,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
         }
 
+        if (pinDataTypes.filters && activeItem.presPinDocFilters !== undefined) {
+            if (bestTarget.docFilters !== activeItem.presPinDocFilters) {
+                bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presPinDocFilters as ObjectField) || new List<string>([]);
+                changed = true;
+            }
+        }
+
         if (pinDataTypes.scrollable) {
             if (bestTarget._scrollTop !== activeItem.presPinViewScroll) {
                 bestTarget._scrollTop = activeItem.presPinViewScroll;
@@ -483,6 +503,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth;
             if (pinProps.pinData.poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(targetDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
             if (pinProps.pinData.viewType) pinDoc.presPinViewType = targetDoc._viewType;
+            if (pinProps.pinData.filters) pinDoc.presPinDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
             if (pinProps.pinData.pannable) {
                 pinDoc.presPinViewX = NumCast(targetDoc._panX);
                 pinDoc.presPinViewY = NumCast(targetDoc._panY);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 9610a71ac..2db44788f 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -182,7 +182,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, this._scrollHeight);
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
-                else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
+                else if (!options.instant && !options.preview) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
                 else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
             }
         } else {
-- 
cgit v1.2.3-70-g09d2


From 9c63f3833f15cc995c2255b83923384686127f3e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 1 Mar 2023 11:57:05 -0500
Subject: changed inPlace link following to be lightbox.  allowed collections
 to be labeled as lightboxes.  lightbox collections display link targets in an
 overlay that hides the rest of their content

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/util/DocumentManager.ts                 | 28 ++++++----
 src/client/util/LinkFollower.ts                    | 10 ++--
 src/client/views/DocumentDecorations.tsx           | 13 ++---
 src/client/views/MainView.tsx                      |  1 -
 src/client/views/PropertiesButtons.tsx             | 62 +++++++++++-----------
 src/client/views/PropertiesView.tsx                |  1 -
 src/client/views/collections/CollectionView.tsx    |  2 +-
 src/client/views/collections/TabDocView.tsx        | 22 ++++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +-
 src/client/views/nodes/DocumentView.tsx            | 17 +++---
 src/client/views/nodes/MapBox/MapBox.tsx           |  1 -
 src/fields/documentSchemas.ts                      |  4 +-
 13 files changed, 88 insertions(+), 78 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d2ab67849..70c57e8fa 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -160,6 +160,7 @@ export class DocumentOptions {
     _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
     _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target
     _showTitle?: string; // field name to display in header (:hover is an optional suffix)
+    _isLightbox?: boolean; // whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target
     _showCaption?: string; // which field to display in the caption area.  leave empty to have no caption
     _scrollTop?: number; // scroll location for pdfs
     _noAutoscroll?: boolean; // whether collections autoscroll when this item is dragged
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d9273c2c9..0d10bed43 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -10,7 +10,7 @@ import { CollectionDockingView } from '../views/collections/CollectionDockingVie
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { CollectionView } from '../views/collections/CollectionView';
 import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
@@ -307,16 +307,22 @@ export class DocumentManager {
             }
             //}
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
-            const doFocus = (forceDidFocus: boolean) =>
-                focusView.focus(originalTarget, {
-                    ...options,
-                    originalTarget,
-                    afterFocus: (didFocus: boolean) =>
-                        new Promise<ViewAdjustment>(res => {
-                            focusAndFinish(forceDidFocus || didFocus);
-                            res(ViewAdjustment.doNothing);
-                        }),
-                });
+            const doFocus = (forceDidFocus: boolean) => {
+                if (options.openInLightbox || (!options.originatingDoc?.followLinkLocation && DocCast(focusView.rootDoc.context)?._isLightbox)) {
+                    focusView.props.addDocTab(targetDoc, OpenWhere.lightbox);
+                    focusAndFinish(true);
+                } else {
+                    focusView.focus(originalTarget, {
+                        ...options,
+                        originalTarget,
+                        afterFocus: (didFocus: boolean) =>
+                            new Promise<ViewAdjustment>(res => {
+                                focusAndFinish(forceDidFocus || didFocus);
+                                res(ViewAdjustment.doNothing);
+                            }),
+                    });
+                }
+            };
             if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) {
                 focusView.iconify(() => doFocus(true));
             } else {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 50bc89ed2..e86e68c21 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -3,6 +3,7 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
@@ -36,7 +37,9 @@ export class LinkFollower {
             const createTabForTarget = (didFocus: boolean) =>
                 new Promise<ViewAdjustment>(res => {
                     const where = LightboxView.LightboxDoc ? OpenWhere.lightbox : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
-                    docViewProps.addDocTab(doc, where);
+                    const lightbox = where === OpenWhere.lightbox && DocumentManager.GetContextPath(doc).find(container => container.isLightbox && DocumentManager.Instance.getDocumentView(container));
+                    const addDocTab = lightbox ? DocumentManager.Instance.getDocumentView(lightbox)?.ComponentView?.addDocTab : undefined;
+                    (addDocTab ?? docViewProps.addDocTab)(doc, where);
                     setTimeout(() => {
                         const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
                         if (targDocView) {
@@ -51,7 +54,7 @@ export class LinkFollower {
                             });
                         } else {
                             finished?.();
-                            res(where !== OpenWhere.inPlace || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
+                            res(BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing);
                         }
                     }, 100);
                 });
@@ -109,6 +112,7 @@ export class LinkFollower {
                         zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkTransitionTime, 500),
                         zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null),
                         easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
+                        openInLightbox: sourceDoc.followLinkLocation === OpenWhere.lightbox,
                         effect: sourceDoc,
                         originatingDoc: sourceDoc,
                         zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText),
@@ -123,7 +127,7 @@ export class LinkFollower {
                     } else {
                         const containerDocContext = DocumentManager.GetContextPath(target);
                         const targetContexts = !sourceDoc.followLinkToOuterContext && containerDocContext.length ? [containerDocContext.lastElement()] : containerDocContext;
-                        DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.inPlace), finished), targetContexts, allFinished);
+                        DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.lightbox), finished), targetContexts, allFinished);
                     }
                 };
                 let movedTarget = false;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4e51f10a8..eeef01d17 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,7 +27,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
-import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
@@ -283,11 +283,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
-                LightboxView.SetLightboxDoc(
-                    openDoc,
-                    undefined,
-                    selectedDocs.slice(1).map(view => view.props.Document)
-                );
+                selectedDocs[0].props.addDocTab(openDoc, OpenWhere.lightbox);
+                // LightboxView.SetLightboxDoc(
+                //     openDoc,
+                //     undefined,
+                //     selectedDocs.slice(1).map(view => view.props.Document)
+                // );
             }
         }
         SelectionManager.DeselectAll();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 1e9220e1e..95c0f3755 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -692,7 +692,6 @@ export class MainView extends React.Component {
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
         // prettier-ignore
         switch (whereFields[0]) {
-            case OpenWhere.inPlace:  // fall through to lightbox
             case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 7e33ee495..a5c01490f 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -7,7 +7,7 @@ import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
-import { BoolCast, StrCast } from '../../fields/Types';
+import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
@@ -20,6 +20,7 @@ import { VideoBox } from './nodes/VideoBox';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
 import React = require('react');
+import { LinkManager } from '../util/LinkManager';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -118,14 +119,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => 'object-group'
         );
     }
-    // this implments a container pattern by marking the targetDoc (collection) as an inPlace container,
-    // and then making the contained collection be a "menu" such that when any of its contents are clicked,
-    // they will open their targets in the outer container.  To get back to the "menu", you click on the main container.
-    @computed get inPlaceContainerButton() {
+    // this implments a container pattern by marking the targetDoc (collection) as a lightbox
+    // that always fits its contents to its container and that hides all other documents when
+    // a link is followed that targets a 'lightbox' destination
+    @computed get isLightboxButton() {
         return this.propertyToggleBtn(
-            'In Place',
-            'isInPlaceContainer',
-            on => `${on ? 'Make' : 'Remove'} in place container flag`,
+            'Lightbox',
+            'isLightbox',
+            on => `${on ? 'Set' : 'Remove'} lightbox flag`,
             on => 'window-restore',
             onClick => {
                 SelectionManager.Views().forEach(dv => {
@@ -134,16 +135,12 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     // containerDoc.noShadow =
                     // containerDoc.noHighlighting =
                     // containerDoc._forceActive =
-                    containerDoc._fitContentsToBox = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
-                    containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
+                    containerDoc._fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox;
+                    containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
                     dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
                     containerContents.forEach(doc => {
-                        DocListCast(doc.links).forEach(link => {
-                            doc.isLinkButton = true;
-                            //doc.followLinkLocation = OpenWhere.inPlace;
-                            link.linkDisplay = false;
-                        });
+                        DocListCast(doc.links).forEach(link => (link.linkDisplay = false));
                     });
                 });
             }
@@ -290,7 +287,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     @action
     handleOptionChange = (onClick: string) => {
-        this.selectedDoc && (this.selectedDoc.onClickBehavior = onClick);
         SelectionManager.Views()
             .filter(dv => dv.docView)
             .map(dv => dv.docView!)
@@ -305,7 +301,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         break;
                     case 'linkInPlace':
                         docView.toggleFollowLink(false, false);
-                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.inPlace : undefined;
+                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.lightbox : undefined;
                         break;
                     case 'linkOnRight':
                         docView.toggleFollowLink(false, false);
@@ -327,24 +323,26 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             ['nothing', 'Select Document'],
             ['enterPortal', 'Enter Portal'],
             ['toggleDetail', 'Toggle Detail'],
-            ['linkInPlace', 'Open in Place'],
+            ['linkInPlace', 'Open Link in Lightbox'],
             ['linkOnRight', 'Open Link on Right'],
         ];
-        const currentSelection = this.selectedDoc.onClickBehavior;
-        // Get items to place into the list
-
         const list = buttonList.map(value => {
-            const click = () => {
-                this.handleOptionChange(value[0]);
-            };
+            const click = () => this.handleOptionChange(value[0]);
+            const linkButton = BoolCast(this.selectedDoc._isLinkButton);
+            const followLoc = this.selectedDoc._followLinkLocation;
+            const linkedToLightboxView = () => DocListCast(this.selectedDoc.links).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+
+            let active = false;
+            // prettier-ignore
+            switch (value[0]) {
+                case 'linkInPlace': active = linkButton && followLoc === OpenWhere.lightbox && !linkedToLightboxView();  break;
+                case 'linkOnRight': active = linkButton && followLoc === OpenWhere.addRight; break;
+                case 'enterPortal': active = linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break;
+                case 'toggleDetail':active = ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break;
+                case 'nothing':     active = !linkButton && this.selectedDoc.onClick === undefined;break;
+            }
             return (
-                <div
-                    className="list-item"
-                    key={`${value}`}
-                    style={{
-                        backgroundColor: value[0] === currentSelection ? Colors.LIGHT_BLUE : undefined,
-                    }}
-                    onClick={click}>
+                <div className="list-item" key={`${value}`} style={{ backgroundColor: active ? Colors.LIGHT_BLUE : undefined }} onClick={click}>
                     {value[1]}
                 </div>
             );
@@ -413,7 +411,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.freezeThumb)}
                 {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
-                {toggle(this.inPlaceContainerButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
+                {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
                 {toggle(this.maskButton, { display: !isInk ? 'none' : '' })}
                 {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e9d1433c3..0fe165783 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1626,7 +1626,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <option value={OpenWhere.add}>Opening in new tab</option>
                                                 <option value={OpenWhere.replace}>Replacing current tab</option>
                                                 <option value={OpenWhere.inParent}>Opening in same collection</option>
-                                                <option value={OpenWhere.inPlace}>Opening in place</option>
                                                 {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
                                             </select>
                                         </div>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 90406ed3c..316f1e4e9 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -191,7 +191,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) {
                 optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
-            !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' });
+            !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' });
 
             if (!Doc.noviceMode && false) {
                 optionItems.push({
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 01872df84..042c2b71b 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -350,7 +350,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     //  replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents,
     //                                  "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name,
     //                                   "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right
-    //  inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack
+    //  lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
     addDocTab = (doc: Doc, location: OpenWhere) => {
         SelectionManager.DeselectAll();
         const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
@@ -359,17 +359,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         // prettier-ignore
         switch (whereFields[0]) {
             case undefined:
-            case OpenWhere.inPlace:    {
-                if (this.layoutDoc?.isInPlaceContainer) {
-                    const inPlaceView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
-                    const data = inPlaceView?.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)];
-                    if (inPlaceView && (!data || data instanceof List)) {
-                        inPlaceView.layoutDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
-                        return true;
-                    }
-                }
-            }
-            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
+            case OpenWhere.lightbox:   if (this.layoutDoc?._isLightbox) {
+                                            const lightboxView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
+                                            const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)];
+                                            if (lightboxView && (!data || data instanceof List)) {
+                                                lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)] = new List<Doc>([doc]);
+                                                return true;
+                                            }
+                                       }
+                                       return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
             case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 78804b070..be8a72369 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1414,8 +1414,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     false
                 );
             case undefined:
-            case OpenWhere.inPlace:
-                if (this.layoutDoc.isInPlaceContainer) {
+            case OpenWhere.lightbox:
+                if (this.layoutDoc._isLightbox) {
                     this.layoutDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
                     return true;
                 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index beb0f8e3a..8257c9c49 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -72,7 +72,6 @@ export enum ViewAdjustment {
 }
 
 export enum OpenWhere {
-    inPlace = 'inPlace',
     lightbox = 'lightbox',
     add = 'add',
     addLeft = 'add:left',
@@ -109,6 +108,7 @@ export interface DocFocusOptions {
     effect?: Doc; // animation effect for focus
     noSelect?: boolean; // whether target should be selected after focusing
     playAudio?: boolean; // whether to play audio annotation on focus
+    openInLightbox?: boolean; // whether to open target in lightbox or just focus on it
     zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
     toggleTarget?: boolean; // whether to toggle target on and off
     originatingDoc?: Doc; // document that triggered the focus
@@ -123,6 +123,7 @@ export interface DocComponentView {
     scrollPreview?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     scrollFocus?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
+    addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
     menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
@@ -640,7 +641,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         );
                     UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
                 } else if (!Doc.IsSystem(this.rootDoc) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) {
-                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
+                    //UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
+                    UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
@@ -870,13 +872,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    makeIntoPortal = async () => {
+    makeIntoPortal = () => {
         const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
         if (!portalLink) {
-            const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' });
-            DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from');
+            DocUtils.MakeLink(
+                { doc: this.props.Document },
+                { doc: Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }) },
+                'portal to:portal from'
+            );
         }
-        this.Document.followLinkLocation = OpenWhere.inPlace;
+        this.Document.followLinkLocation = OpenWhere.lightbox;
         this.Document.followLinkZoom = true;
         this.Document._isLinkButton = true;
     };
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 039f11cd1..c182dfd19 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -331,7 +331,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         docs.forEach(doc => {
             if (doc.lat !== undefined && doc.lng !== undefined) {
                 const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
-                doc.onClickBehavior = 'enterPortal';
                 if (existingMarker) {
                     Doc.AddDocToList(existingMarker, 'data', doc);
                 } else {
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 10324449f..5b489a96c 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -84,11 +84,11 @@ export const documentSchema = createSchema({
     onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onDragStart: ScriptField, // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
-    followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, )
+    followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, )
     hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
     hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
     linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
-    isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations
+    isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations
     isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
     layers: listSpec('string'), // which layers the document is part of
     _lockedPosition: 'boolean', // whether the document can be moved (dragged)
-- 
cgit v1.2.3-70-g09d2


From 9c29092fc9df29c02cc83885e4ba5645b71867d4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 1 Mar 2023 15:45:03 -0500
Subject: fix to notetakingiview resizing when deleting a column to force an
 unset column

---
 src/client/views/DocumentDecorations.tsx                     |  2 +-
 src/client/views/collections/CollectionNoteTakingView.tsx    | 12 ++++++------
 .../views/collections/CollectionNoteTakingViewColumn.tsx     |  7 +++----
 3 files changed, 10 insertions(+), 11 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index eeef01d17..2982f8a99 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -797,7 +797,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
 
         // Radius constants
-        const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox;
+        const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
         const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding));
         const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2);
         const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index edd5ba96c..0e1601f35 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -55,11 +55,11 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
         if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
             setTimeout(() => {
-                const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
+                const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null));
                 const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
-                if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
-                    if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
-                    else this.dataDoc.columnHeaders = new List<SchemaHeaderField>();
+                if (needsUnsetCategory || columnHeaders.length === 0) {
+                    columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
+                    this.resizeColumns(columnHeaders);
                 }
             });
         }
@@ -321,7 +321,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375].
     // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33]
     @action
-    resizeColumns = (isAdd: boolean, headers: SchemaHeaderField[], colIndex: number) => {
+    resizeColumns = (headers: SchemaHeaderField[]) => {
         const n = headers.length;
         const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0);
         const scaleFactor = 1 / curWidths;
@@ -563,7 +563,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const columnHeaders = Array.from(Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null));
         const newColWidth = 1 / (this.numGroupColumns + 1);
         columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth));
-        value && this.resizeColumns(true, columnHeaders, this.columnHeaders.length - 1);
+        value && this.resizeColumns(columnHeaders);
         return true;
     };
 
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index ec6a2bec8..829d055e5 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -44,7 +44,7 @@ interface CSVFieldColumnProps {
     observeHeight: (myref: any) => void;
     unobserveHeight: (myref: any) => void;
     editableViewProps: () => any;
-    resizeColumns: (isAdd: boolean, headers: SchemaHeaderField[], colIndex: number) => boolean;
+    resizeColumns: (headers: SchemaHeaderField[]) => boolean;
     maxColWidth: number;
     dividerWidth: number;
     availableWidth: number;
@@ -142,10 +142,9 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
     deleteColumn = () => {
         const columnHeaders = Array.from(Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null));
         if (this.props.headingObject) {
-            const index = columnHeaders.indexOf(this.props.headingObject);
             this.props.docList.forEach(d => (d[this.props.pivotField] = undefined));
-            columnHeaders.splice(index, 1);
-            this.props.resizeColumns(false, columnHeaders, index);
+            columnHeaders.splice(columnHeaders.indexOf(this.props.headingObject), 1);
+            this.props.resizeColumns(columnHeaders);
         }
     };
 
-- 
cgit v1.2.3-70-g09d2


From 8ab9236664c561d54d6a41ecb1eb2eaf6064fc0c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 27 Mar 2023 18:21:13 -0400
Subject: changed longPress to always select and to show decorations.  fixed
 single/double-click code and cleaned up behavior timeouts.  fixed pointer
 events for tree view editing titles and using as powerpoint.

---
 src/client/documents/Documents.ts                  |  11 +-
 src/client/util/DragManager.ts                     |  34 ++--
 src/client/views/DocumentDecorations.tsx           |   3 +-
 src/client/views/InkingStroke.tsx                  |   1 -
 src/client/views/MainView.tsx                      |  17 ++
 .../collections/CollectionNoteTakingViewColumn.tsx |   4 +-
 src/client/views/collections/TabDocView.tsx        |   2 +-
 src/client/views/collections/TreeView.tsx          |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   3 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   6 +-
 src/client/views/nodes/DocumentView.tsx            | 197 ++++++++++-----------
 src/client/views/nodes/KeyValueBox.tsx             |  24 ++-
 src/client/views/nodes/LinkBox.tsx                 |  54 +++---
 src/client/views/nodes/ScriptingBox.tsx            |   4 +-
 src/client/views/nodes/WebBox.tsx                  |   3 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   2 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  11 +-
 src/client/views/pdf/AnchorMenu.tsx                |   2 +-
 18 files changed, 201 insertions(+), 183 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 0b6d4380d..2bc1b5b1d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -228,7 +228,8 @@ export class DocumentOptions {
     contextMenuScripts?: List<ScriptField>;
     contextMenuLabels?: List<string>;
     contextMenuIcons?: List<string>;
-    allowClickBeforeDoubleClick?: boolean; // whether a click function can fire before the timeout for a double click has expired
+    defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
+    waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire.  'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
     dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
     description?: string; // added for links
     layout?: string | Doc; // default layout string for a document
@@ -408,6 +409,7 @@ export namespace Docs {
                         nativeDimModifiable: true,
                         nativeHeightUnfrozen: true,
                         forceReflow: true,
+                        defaultDoubleClick: 'ignore',
                     },
                 },
             ],
@@ -436,7 +438,7 @@ export namespace Docs {
                 DocumentType.WEB,
                 {
                     layout: { view: WebBox, dataField: defaultDataKey },
-                    options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
+                    options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always' },
                 },
             ],
             [
@@ -578,14 +580,14 @@ export namespace Docs {
                 DocumentType.PRES,
                 {
                     layout: { view: PresBox, dataField: defaultDataKey },
-                    options: {},
+                    options: { defaultDoubleClick: 'ignore' },
                 },
             ],
             [
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: 'icon' },
-                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40 },
+                    options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', hideLinkButton: true, _width: 40, _height: 40 },
                 },
             ],
             [
@@ -983,6 +985,7 @@ export namespace Docs {
             I.author = Doc.CurrentUserEmail;
             I.rotation = 0;
             I.data = new InkField(points);
+            I.defaultDoubleClick = 'click';
             I.creationDate = new DateField();
             I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             //I['acl-Override'] = SharingPermissions.Unset;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c35941ee5..cc116fb46 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
 import { action, observable, runInAction } from 'mobx';
 import { DateField } from '../../fields/DateField';
-import { Doc, Field, Opt } from '../../fields/Doc';
+import { Doc, Field, Opt, StrListCast } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { PrefetchProxy } from '../../fields/Proxy';
 import { listSpec } from '../../fields/Schema';
@@ -207,24 +207,26 @@ export namespace DragManager {
             dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
             if (docDragData && !docDragData.droppedDocuments.length) {
                 docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
-                docDragData.droppedDocuments = await Promise.all(
-                    dragData.draggedDocuments.map(async d =>
-                        !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
-                            ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
-                            : docDragData.dropAction === 'alias'
-                            ? Doc.BestAlias(d)
-                            : docDragData.dropAction === 'proto'
-                            ? Doc.GetProto(d)
-                            : docDragData.dropAction === 'copy'
-                            ? (
-                                  await Doc.MakeClone(d)
-                              ).clone
-                            : d
+                docDragData.droppedDocuments = (
+                    await Promise.all(
+                        dragData.draggedDocuments.map(async d =>
+                            !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
+                                ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
+                                : docDragData.dropAction === 'alias'
+                                ? Doc.BestAlias(d)
+                                : docDragData.dropAction === 'proto'
+                                ? Doc.GetProto(d)
+                                : docDragData.dropAction === 'copy'
+                                ? (
+                                      await Doc.MakeClone(d)
+                                  ).clone
+                                : d
+                        )
                     )
-                );
+                ).filter(d => d);
                 !['same', 'proto'].includes(docDragData.dropAction as any) &&
                     docDragData.droppedDocuments.forEach((drop: Doc, i: number) => {
-                        const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec('string'), []);
+                        const dragProps = StrListCast(dragData.draggedDocuments[i].removeDropProperties);
                         const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps));
                         remProps.map(prop => (drop[prop] = undefined));
                     });
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2982f8a99..2d2d3c2f6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -67,8 +67,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         reaction(
             () => SelectionManager.Views().slice(),
             action(docs => {
-                this._showNothing = true;
-                docs.length > 1 && (this._showNothing = false); // show decorations if multiple docs are selected
+                this._showNothing = !DocumentView.LongPress && docs.length === 1; // show decorations if multiple docs are selected or we're long pressing
                 this._editingTitle = false;
             })
         );
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 3861331b5..a085b69a5 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -179,7 +179,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
                       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;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a30d139be..c84d204d5 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -214,6 +214,8 @@ export class MainView extends React.Component {
         window.removeEventListener('keyup', KeyManager.Instance.unhandle);
         window.removeEventListener('keydown', KeyManager.Instance.handle);
         window.removeEventListener('pointerdown', this.globalPointerDown, true);
+        window.removeEventListener('pointermove', this.globalPointerMove, true);
+        window.removeEventListener('mouseclick', this.globalPointerClick, true);
         window.removeEventListener('paste', KeyManager.Instance.paste as any);
         document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
     }
@@ -485,7 +487,20 @@ export class MainView extends React.Component {
         );
     }
 
+    private longPressTimer: NodeJS.Timeout | undefined;
+    globalPointerClick = action((e: any) => {
+        this.longPressTimer && clearTimeout(this.longPressTimer);
+        DocumentView.LongPress = false;
+    });
+    globalPointerMove = action((e: PointerEvent) => {
+        if (e.movementX > 3 || e.movementY > 3) this.longPressTimer && clearTimeout(this.longPressTimer);
+    });
     globalPointerDown = action((e: PointerEvent) => {
+        DocumentView.LongPress = false;
+        this.longPressTimer = setTimeout(
+            action(() => (DocumentView.LongPress = true)),
+            1000
+        );
         DocumentManager.removeOverlayViews();
         Doc.linkFollowUnhighlight();
         AudioBox.Enabled = true;
@@ -506,6 +521,8 @@ export class MainView extends React.Component {
         window.addEventListener('dragover', e => e.preventDefault(), false);
         // document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined));
         document.addEventListener('pointerdown', this.globalPointerDown, true);
+        document.addEventListener('pointermove', this.globalPointerMove, true);
+        document.addEventListener('mouseclick', this.globalPointerClick, true);
         document.addEventListener(
             'click',
             (e: MouseEvent) => {
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 621e3d93b..28bdd0cb9 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -268,7 +268,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
                             {this.props.renderChildren(this.props.docList)}
                         </div>
 
-                        {!this.props.chromeHidden && type !== DocumentType.PRES ? (
+                        {!this.props.chromeHidden ? (
                             <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}>
                                 <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
                                     <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
@@ -289,7 +289,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
         const heading = this._heading;
         return (
             <div
-                className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')}
+                className="collectionNoteTakingViewFieldColumn"
                 key={heading}
                 style={{
                     width: this.columnWidth,
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 631b9add5..5e6849e7c 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -422,7 +422,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                         this._lastView = this._view;
                     })}
                     renderDepth={0}
-                    LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString('data') : undefined}
+                    LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString() : undefined}
                     Document={this._document}
                     DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined}
                     ContainingCollectionView={undefined}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index af2d148e0..257428d56 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -305,7 +305,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
 
     public static makeTextBullet() {
-        const bullet = Docs.Create.TextDocument('-text-', {
+        const bullet = Docs.Create.TextDocument('', {
             layout: CollectionView.LayoutString('data'),
             title: '-title-',
             treeViewExpandedViewLock: true,
@@ -326,7 +326,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         });
         Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text');
         Doc.GetProto(bullet).data = new List<Doc>([]);
-        FormattedTextBox.SelectOnLoad = bullet[Id];
+        DocumentManager.Instance.AddViewRenderedCb(bullet, dv => dv.ComponentView?.setFocus?.());
+
         return bullet;
     }
 
@@ -902,6 +903,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 hideDecorationTitle={this.props.treeView.outlineMode}
                 hideResizeHandles={this.props.treeView.outlineMode}
                 styleProvider={this.titleStyleProvider}
+                onClickScriptDisable="never"
                 docViewPath={returnEmptyDoclist}
                 treeViewDoc={this.props.treeView.props.Document}
                 addDocument={undefined}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index be4c2a60e..8104ab1a7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -822,7 +822,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             Doc.ActiveTool = InkTool.None;
             if (this.props.isContentActive(true)) e.stopPropagation();
-        } else if (!e.cancelBubble) {
+        } else {
+            this.props.DocumentView?.().docView?.cancelMoveEvents();
             if (this.tryDragCluster(e, this._hitCluster)) {
                 return true;
             }
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index c21bc9754..9e56de8c2 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -127,9 +127,9 @@ export class DocumentContentsView extends React.Component<
         TraceMobx();
         if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString;
         if (!this.layoutDoc) return '<p>awaiting layout</p>';
-        if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString('data'));
+        if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString());
         const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, 'layout')], 'string');
-        if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(this.layoutDoc.proto ? 'proto' : '');
+        if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString();
         if (typeof layout === 'string') return layout;
         return '<p>Loading layout</p>';
     }
@@ -273,7 +273,7 @@ export class DocumentContentsView extends React.Component<
                 jsx={layoutFrame}
                 showWarnings={true}
                 onError={(test: any) => {
-                    console.log('DocumentContentsView:' + test);
+                    console.log('DocumentContentsView:' + test, bindings, layoutFrame);
                 }}
             />
         );
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 42c2b28ba..c8f677c5f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -124,6 +124,7 @@ export interface DocComponentView {
     select?: (ctrlKey: boolean, shiftKey: boolean) => void;
     menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
     isAnyChildContentActive?: () => boolean; // is any child content of the document active
+    onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
     getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
     setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
     playFrom?: (time: number, endTime?: number) => void;
@@ -133,6 +134,7 @@ export interface DocComponentView {
     setFocus?: () => void; // sets input focus to the componentView
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
     incrementalRendering?: () => void;
+    fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox)
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
@@ -192,6 +194,7 @@ export interface DocumentViewSharedProps {
     ignoreAutoHeight?: boolean;
     forceAutoHeight?: boolean;
     disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
+    onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
     pointerEvents?: () => Opt<string>;
     scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
     createNewFilterDoc?: () => void;
@@ -245,9 +248,10 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
 @observer
 export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
-    private _cursorTimer: NodeJS.Timeout | undefined;
-    private _longPress = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    private _doubleClickTimeout: NodeJS.Timeout | undefined;
+    private _singleClickFunc: undefined | (() => any);
+    private _longPressSelector: NodeJS.Timeout | undefined;
     private _downX: number = 0;
     private _downY: number = 0;
     private _downTime: number = 0;
@@ -257,7 +261,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private _doubleTap = false;
     private _mainCont = React.createRef<HTMLDivElement>();
     private _titleRef = React.createRef<EditableView>();
-    private _timeout: NodeJS.Timeout | undefined;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
     protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@@ -265,8 +268,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
-    @observable _pendingDoubleClick = false;
-    @observable _cursorPress = false;
 
     private get topMost() {
         return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
@@ -331,9 +332,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     @computed get nativeHeight() {
         return this.props.NativeHeight();
     }
+    @computed get disableClickScriptFunc() {
+        return (
+            DocumentView.LongPress ||
+            this.props.onClickScriptDisable === 'always' ||
+            this._componentView?.onClickScriptDisable?.() ||
+            this._componentView?.isAnyChildContentActive?.()
+        );
+    }
     @computed get onClickHandler() {
-        const noClick = this.props.LayoutTemplateString?.includes(KeyValueBox.name) || this.rootSelected() || this.props.isSelected() || this._componentView?.isAnyChildContentActive?.();
-        return noClick ? undefined : this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
+        return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
     }
     @computed get onDoubleClickHandler() {
         return this.props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick;
@@ -446,7 +454,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
             const touch = me.touchEvent.changedTouches.item(0);
             if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
-                if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
+                if (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) {
                     this.cleanUpInteractions();
                     this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined);
                 }
@@ -585,91 +593,76 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             let stopPropagate = true;
             let preventDefault = true;
-            const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name);
             (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
-            if (this._doubleTap && (![DocumentType.FONTICON, DocumentType.PRES].includes(this.props.Document.type as any) || this.onDoubleClickHandler)) {
-                // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
-                if (this._timeout) {
-                    clearTimeout(this._timeout);
-                    this._pendingDoubleClick = false;
-                    this._timeout = undefined;
+            if (this._doubleTap) {
+                if (this.onDoubleClickHandler?.script) {
+                    const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables
+                    // prettier-ignore
+                    const func = () => this.onDoubleClickHandler.script.run( {
+                            this: this.layoutDoc,
+                            self: this.rootDoc,
+                            scriptContext: this.props.scriptContext,
+                            thisContainer: this.props.ContainingCollectionDoc,
+                            documentView: this.props.DocumentView(),
+                            clientX, clientY,  altKey,  shiftKey,  ctrlKey,
+                            value: undefined,
+                        }, console.log  );
+                    UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
+                } else if (!Doc.IsSystem(this.rootDoc) && (this.Document.defaultDoubleClick === undefined || this.Document.defaultDoubleClick === 'default')) {
+                    UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
+                    SelectionManager.DeselectAll();
+                    Doc.UnBrushDoc(this.props.Document);
+                } else {
+                    this._singleClickFunc?.();
                 }
-                if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) {
-                    // bcz: hack? don't execute script if you're clicking on a scripting box itself
-                    const { clientX, clientY, shiftKey, altKey, ctrlKey } = e;
+                this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+                this._doubleClickTimeout = undefined;
+                this._singleClickFunc = undefined;
+            } else {
+                let clickFunc: undefined | (() => any);
+                if (!this.disableClickScriptFunc && this.onClickHandler?.script) {
+                    const { clientX, clientY, shiftKey, altKey, metaKey } = e;
                     const func = () =>
-                        this.onDoubleClickHandler.script.run(
+                        this.onClickHandler?.script.run(
                             {
                                 this: this.layoutDoc,
                                 self: this.rootDoc,
+                                _readOnly_: false,
                                 scriptContext: this.props.scriptContext,
                                 thisContainer: this.props.ContainingCollectionDoc,
                                 documentView: this.props.DocumentView(),
                                 clientX,
                                 clientY,
-                                altKey,
                                 shiftKey,
-                                ctrlKey,
-                                value: undefined,
+                                altKey,
+                                metaKey,
                             },
                             console.log
-                        );
-                    UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
-                } else if (!Doc.IsSystem(this.rootDoc) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) {
-                    //UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
-                    UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
-                    SelectionManager.DeselectAll();
-                    Doc.UnBrushDoc(this.props.Document);
+                        ).result?.select === true
+                            ? this.props.select(false)
+                            : '';
+                    clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
+                } else if (!this.disableClickScriptFunc && this.allLinks.length && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+                    clickFunc = () => {
+                        SelectionManager.DeselectAll();
+                        LinkFollower.FollowLink(undefined, this.Document, e.altKey);
+                    };
+                } else {
+                    if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
+                        // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
+                        stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+                    }
+                    preventDefault = false;
                 }
-            } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) {
-                // bcz: hack? don't execute script if you're clicking on a scripting box itself
-                const { clientX, clientY, shiftKey, altKey, metaKey } = e;
-                const func = () =>
-                    this.onClickHandler?.script.run(
-                        {
-                            this: this.layoutDoc,
-                            self: this.rootDoc,
-                            _readOnly_: false,
-                            scriptContext: this.props.scriptContext,
-                            thisContainer: this.props.ContainingCollectionDoc,
-                            documentView: this.props.DocumentView(),
-                            clientX,
-                            clientY,
-                            shiftKey,
-                            altKey,
-                            metaKey,
-                        },
-                        console.log
-                    ).result?.select === true
-                        ? this.props.select(false)
-                        : '';
-                const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
-                if (this.onDoubleClickHandler && !this.props.Document.allowClickBeforeDoubleClick) {
-                    runInAction(() => (this._pendingDoubleClick = true));
-                    this._timeout = setTimeout(() => {
-                        this._timeout = undefined;
-                        clickFunc();
-                    }, 150);
-                } else clickFunc();
-            } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
-                SelectionManager.DeselectAll();
-                this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, e.altKey);
-            } else {
-                if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
-                    // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
-                    stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+
+                this._singleClickFunc = clickFunc ?? (() => (this._componentView?.select ?? this.props.select)(e.ctrlKey || e.metaKey, e.shiftKey));
+                if ((clickFunc && this.Document.waitForDoubleClickToClick !== 'never') || this.Document.waitForDoubleClickToClick === 'always') {
+                    this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+                    this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
                 } else {
-                    runInAction(() => (this._pendingDoubleClick = true));
-                    this._timeout = setTimeout(
-                        action(() => {
-                            this._pendingDoubleClick = false;
-                            this._timeout = undefined;
-                        }),
-                        350
-                    );
-                    (this._componentView?.select ?? this.props.select)(e.ctrlKey || e.metaKey, e.shiftKey);
+                    this._singleClickFunc();
+                    this._singleClickFunc = undefined;
                 }
-                preventDefault = false;
             }
             stopPropagate && e.stopPropagation();
             preventDefault && e.preventDefault();
@@ -678,6 +671,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000);
         if (!(e.nativeEvent as any).DownDocView) (e.nativeEvent as any).DownDocView = GestureOverlay.DownDocView = this.props.DocumentView();
         if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
         // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
@@ -689,13 +683,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             }
             return;
         }
-        this._cursorTimer = setTimeout(
-            action(() => {
-                this._cursorPress = true;
-                this.props.select(false);
-            }),
-            1000 // long press required duration
-        );
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._downTime = Date.now();
@@ -719,44 +706,36 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             }
             document.removeEventListener('pointerup', this.onPointerUp);
             document.addEventListener('pointerup', this.onPointerUp);
-        } else {
-            this._cursorTimer && clearTimeout(this._cursorTimer);
-            this._cursorPress = false;
         }
     };
 
     @action
     onPointerMove = (e: PointerEvent): void => {
-        if (e.cancelBubble) return;
+        if (this.layoutDoc._lockedPosition || DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) return;
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
 
-        if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
+        if (((!this.topMost && this.props.isDocumentActive?.()) || this.layoutDoc.onDragStart) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
             if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
-                if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
-                    document.removeEventListener('pointermove', this.onPointerMove);
-                    document.removeEventListener('pointerup', this.onPointerUp);
-                    this._cursorTimer && clearTimeout(this._cursorTimer);
-                    this._cursorPress = false;
-                    this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
-                }
+                this.cleanupPointerEvents();
+                this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
             }
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
             e.preventDefault();
         }
     };
 
+    cancelMoveEvents = () => document.removeEventListener('pointermove', this.onPointerMove);
+
     cleanupPointerEvents = () => {
         this.cleanUpInteractions();
-        document.removeEventListener('pointermove', this.onPointerMove);
+        this.cancelMoveEvents();
         document.removeEventListener('pointerup', this.onPointerUp);
     };
 
     @action
     onPointerUp = (e: PointerEvent): void => {
         this.cleanupPointerEvents();
-        this._longPress = this._cursorPress;
-        this._cursorTimer && clearTimeout(this._cursorTimer);
-        this._cursorPress = false;
+        this._longPressSelector && clearTimeout(this._longPressSelector);
 
         const now = Date.now();
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
@@ -766,6 +745,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             // bcz: this is a placeholder.  documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
             if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
+        if (DocumentView.LongPress) e.preventDefault();
     };
 
     @undoBatch
@@ -1051,7 +1031,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         // Add link to help documentation
         if (documentationDescription && documentationLink) {
-            console.log('add documentation item');
             helpItems.push({
                 description: documentationDescription,
                 event: () => {
@@ -1078,7 +1057,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
     panelHeight = () => this.props.PanelHeight() - this.headerMargin;
     screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
-    onClickFunc = () => this.onClickHandler as any as ScriptField; // bcz: typing HACK.  check and fix.
+    onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
     setHeight = (height: number) => (this.layoutDoc._height = height);
     setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
     isContentActive = (outsideReaction?: boolean) => {
@@ -1101,7 +1080,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 .concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
                 .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
         const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc));
-        return !this.props.isSelected() &&
+        return !this.props.LayoutTemplateString &&
+            !this.props.isSelected() &&
             LightboxView.LightboxDoc !== this.rootDoc &&
             this.thumb &&
             !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
@@ -1129,7 +1109,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             </Tooltip>
         );
     }
-    contentPointerEvents = () => (this.onClickHandler ? 'none' : this.pointerEvents);
+    contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents);
     @computed get contents() {
         TraceMobx();
         return (
@@ -1326,7 +1306,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             <div
                 className="documentView-captionWrapper"
                 style={{
-                    pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+                    pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
                     minWidth: 50 * ffscale(),
                     maxHeight: `max(100%, ${20 * ffscale()}px)`,
                 }}>
@@ -1360,7 +1340,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
                     color: lightOrDark(background),
                     background,
-                    pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+                    pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
                 }}>
                 <EditableView
                     ref={this._titleRef}
@@ -1420,7 +1400,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         ...style,
                         background: isButton || thumb ? undefined : this.backgroundColor,
                         opacity: this.opacity,
-                        cursor: this._cursorPress ? 'wait' : Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
+                        cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
                         color: StrCast(this.layoutDoc.color, 'inherit'),
                         fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
                         fontSize: Cast(this.Document._fontSize, 'string', null),
@@ -1428,7 +1408,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
                     }}>
                     {this.innards}
-                    {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
+                    {!this.disableClickScriptFunc && this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
                     {this.widgetDecorations ?? null}
                 </div>
             )
@@ -1530,6 +1510,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 @observer
 export class DocumentView extends React.Component<DocumentViewProps> {
     public static ROOT_DIV = 'documentView-effectsWrapper';
+    @observable public static LongPress = false;
     public get displayName() {
         return 'DocumentView(' + this.props.Document?.title + ')';
     } // this makes mobx trace() statements more descriptive
@@ -1628,7 +1609,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return this.docView?.LayoutFieldKey || 'layout';
     }
     get fitWidth() {
-        return this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth;
+        return this.docView?._componentView?.fitWidth?.() ?? this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth;
     }
 
     @computed get hideLinkButton() {
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 0b1de0ff6..9c31ed3e4 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -3,24 +3,24 @@ import { observer } from 'mobx-react';
 import { Doc, Field, FieldResult } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
-import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, DocCast, FieldValue, NumCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
 import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting';
 import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { ImageBox } from './ImageBox';
 import './KeyValueBox.scss';
 import { KeyValuePair } from './KeyValuePair';
 import React = require('react');
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
 import e = require('express');
-import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { ImageBox } from './ImageBox';
-import { OpenWhere } from './DocumentView';
+import { returnTrue } from '../../../Utils';
 
 export type KVPScript = {
     script: CompiledScript;
@@ -30,8 +30,8 @@ export type KVPScript = {
 
 @observer
 export class KeyValueBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldStr: string) {
-        return FieldView.LayoutString(KeyValueBox, fieldStr);
+    public static LayoutString() {
+        return FieldView.LayoutString(KeyValueBox, 'data');
     }
 
     private _mainCont = React.createRef<HTMLDivElement>();
@@ -39,6 +39,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     private _keyInput = React.createRef<HTMLInputElement>();
     private _valInput = React.createRef<HTMLInputElement>();
 
+    componentDidMount() {
+        this.props.setContentView?.(this);
+    }
+    onClickScriptDisable: () => 'always' = () => 'always';
+    fitWidth = returnTrue;
+
     @observable private rows: KeyValuePair[] = [];
 
     @computed get splitPercentage() {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 43f4b43fb..470f7e803 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,29 +1,39 @@
-import React = require("react");
-import { observer } from "mobx-react";
-import { emptyFunction, returnFalse } from "../../../Utils";
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { StyleProp } from "../StyleProvider";
-import { ComparisonBox } from "./ComparisonBox";
+import React = require('react');
+import { observer } from 'mobx-react';
+import { emptyFunction, returnFalse, returnTrue } from '../../../Utils';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
+import { ComparisonBox } from './ComparisonBox';
 import { FieldView, FieldViewProps } from './FieldView';
-import "./LinkBox.scss";
+import './LinkBox.scss';
 
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(LinkBox, fieldKey);
+    }
     isContentActiveFunc = () => this.isContentActive();
+
+    onClickScriptDisable: () => 'always' = () => 'always';
+    componentDidMount() {
+        this.props.setContentView?.(this);
+    }
     render() {
-        if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
-        return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
-            style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} >
-            <ComparisonBox {...this.props}
-                fieldKey="anchor"
-                setHeight={emptyFunction}
-                dontRegisterView={true}
-                renderDepth={this.props.renderDepth + 1}
-                isContentActive={this.isContentActiveFunc}
-                addDocument={returnFalse}
-                removeDocument={returnFalse}
-                moveDocument={returnFalse} />
-        </div>;
+        if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true));
+        return (
+            <div className={`linkBox-container${this.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
+                <ComparisonBox
+                    {...this.props}
+                    fieldKey="anchor"
+                    setHeight={emptyFunction}
+                    dontRegisterView={true}
+                    renderDepth={this.props.renderDepth + 1}
+                    isContentActive={this.isContentActiveFunc}
+                    addDocument={returnFalse}
+                    removeDocument={returnFalse}
+                    moveDocument={returnFalse}
+                />
+            </div>
+        );
     }
-} 
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index a6e2c3e90..5683b0fe7 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -8,7 +8,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { returnEmptyString } from '../../../Utils';
+import { returnEmptyString, returnTrue } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { CompileScript, ScriptParam } from '../../util/Scripting';
@@ -114,6 +114,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
         }
     }
 
+    onClickScriptDisable: () => 'always' = () => 'always';
+
     @action
     componentDidMount() {
         this.props.setContentView?.(this);
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f7425b26a..73283263f 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -205,7 +205,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     this._webPageHasBeenRendered = true;
                 } else if (
                     (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail)
-                    !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty
                     LightboxView.LightboxDoc !== this.rootDoc
                 ) {
                     // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty.
@@ -877,7 +876,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
     };
     @computed get content() {
-        const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
+        const interactive = this.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
         return (
             <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
                 {this.urlContent}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index d9364e5b5..468bcc4d8 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -817,7 +817,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | '
             setMode: () => SetActiveInkWidth(value.toString()),
         }],
         ['strokeColor', {
-            checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.color) : ActiveInkColor()),
+            checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()),
             setInk: (doc: Doc) => (doc.color = String(value)),
             setMode: () => SetActiveInkColor(StrCast(value)),
         }],
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ddec86606..3ce2366f8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1477,7 +1477,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             e.preventDefault();
         }
     };
-    onSelectMove = (e: PointerEvent) => e.stopPropagation();
+    onSelectMove = (e: PointerEvent) => {
+        this.props.DocumentView?.().docView?.cancelMoveEvents();
+        e.stopPropagation();
+    };
     onSelectEnd = (e: PointerEvent) => {
         document.removeEventListener('pointerup', this.onSelectEnd);
         document.removeEventListener('pointermove', this.onSelectMove);
@@ -1497,10 +1500,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 return;
             }
         }
-
-        if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
-            e.stopPropagation();
-        }
     };
     @action
     onDoubleClick = (e: React.MouseEvent): void => {
@@ -1566,8 +1565,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         if (this.props.isSelected(true)) {
             // if text box is selected, then it consumes all click events
             (e.nativeEvent as any).handledByInnerReactInstance = true;
-            if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox.  Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it.
-            // e.stopPropagation();  // bcz: not sure why this was here.  We need to allow the DocumentView to get clicks to process doubleClicks (see above comment)
             this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
         }
         this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index fc8f1da49..7392d2706 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -80,7 +80,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             { fireImmediately: true }
         );
         this._disposer = reaction(
-            () => SelectionManager.Views(),
+            () => SelectionManager.Views().slice(),
             selected => {
                 this._showLinkPopup = false;
                 AnchorMenu.Instance.fadeOut(true);
-- 
cgit v1.2.3-70-g09d2


From dabac69d26c8e8e69bd55466ce221d9e33e36638 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Mar 2023 14:23:17 -0400
Subject: numerous changes to try to simplify event handling in DocumentView -
 got rid of isContentActive in DocComponent since it's in DocumentView.  
 Including adding 'enableDragWhenActive' , 'onClickScriptDisable',

---
 src/Utils.ts                                       |  17 +-
 src/client/documents/Documents.ts                  |   3 +-
 src/client/util/CurrentUserUtils.ts                |  12 +-
 src/client/views/DocComponent.tsx                  |  13 --
 src/client/views/DocumentDecorations.tsx           |   8 +-
 src/client/views/InkingStroke.tsx                  |   2 +-
 src/client/views/MainView.tsx                      |   8 +-
 src/client/views/StyleProvider.tsx                 |   4 +-
 .../views/collections/CollectionStackingView.tsx   |   2 +
 src/client/views/collections/TabDocView.tsx        |   4 +-
 src/client/views/collections/TreeView.tsx          |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 100 +--------
 .../collectionLinear/CollectionLinearView.scss     |  10 +-
 .../collectionLinear/CollectionLinearView.tsx      |  18 +-
 .../collectionSchema/CollectionSchemaView.scss     |   3 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  46 ++--
 .../collections/collectionSchema/SchemaRowBox.tsx  |   8 +-
 src/client/views/global/globalCssVariables.scss    |   3 +-
 .../views/global/globalCssVariables.scss.d.ts      |   2 +-
 src/client/views/nodes/ColorBox.tsx                |   2 +-
 src/client/views/nodes/DocumentView.tsx            | 238 ++++-----------------
 src/client/views/nodes/FunctionPlotBox.tsx         |   2 +-
 src/client/views/nodes/KeyValueBox.tsx             |  15 +-
 src/client/views/nodes/KeyValuePair.tsx            |   9 +-
 src/client/views/nodes/LinkBox.tsx                 |   9 +-
 src/client/views/nodes/ScriptingBox.tsx            |   4 +-
 src/client/views/nodes/WebBox.tsx                  |   2 +-
 src/client/views/nodes/button/FontIconBox.scss     |  10 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   9 +-
 src/client/views/nodes/trails/PresBox.tsx          |   4 +-
 src/client/views/pdf/PDFViewer.tsx                 |   9 -
 src/fields/Doc.ts                                  |   2 +-
 src/fields/ScriptField.ts                          |   2 +-
 34 files changed, 171 insertions(+), 421 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index ae1478943..0c7deaf5d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -8,8 +8,12 @@ import { Message } from './server/Message';
 import Color = require('color');
 
 export namespace Utils {
+    export let CLICK_TIME = 300;
     export let DRAG_THRESHOLD = 4;
     export let SNAP_THRESHOLD = 10;
+    export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) {
+        return Date.now() - downTime < Utils.CLICK_TIME && Math.abs(x - downX) < Utils.DRAG_THRESHOLD && Math.abs(y - downY) < Utils.DRAG_THRESHOLD;
+    }
 
     export function readUploadedFileAsText(inputFile: File) {
         const temporaryFileReader = new FileReader();
@@ -509,11 +513,22 @@ export function returnTrue() {
     return true;
 }
 
+export function returnAlways(): 'always' {
+    return 'always';
+}
+export function returnNever(): 'never' {
+    return 'never';
+}
+
+export function returnDefault(): 'default' {
+    return 'default';
+}
+
 export function returnFalse() {
     return false;
 }
 
-export function returnAll() {
+export function returnAll(): 'all' {
     return 'all';
 }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2bc1b5b1d..c484db0db 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -230,6 +230,7 @@ export class DocumentOptions {
     contextMenuIcons?: List<string>;
     defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
     waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire.  'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
+    enableDragWhenActive?: boolean;
     dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
     description?: string; // added for links
     layout?: string | Doc; // default layout string for a document
@@ -587,7 +588,7 @@ export namespace Docs {
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: 'icon' },
-                    options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', hideLinkButton: true, _width: 40, _height: 40 },
+                    options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', enableDragWhenActive: true, hideLinkButton: true, _width: 40, _height: 40 },
                 },
             ],
             [
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 814b7b072..b21f53221 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -62,7 +62,7 @@ export class CurrentUserUtils {
         const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
             {
                 btnOpts: { title: "slide", icon: "address-card" }, 
-                templateOpts: { _width: 400, _height: 300, title: "slideView", childDocumentsActive: true, _xMargin: 3, _yMargin: 3, system: true }, 
+                templateOpts: { _width: 400, _height: 300, title: "slideView",  _xMargin: 3, _yMargin: 3, system: true }, 
                 template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
                     [
                         Docs.Create.MulticolumnDocument([], { title: "data", _height: 200,  system: true }),
@@ -95,7 +95,7 @@ export class CurrentUserUtils {
 
         const reqdOpts:DocumentOptions = {
             title: "Experimental Tools", _xMargin: 0, _showTitle: "title", _chromeHidden: true,
-            _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true, childDocumentsActive: true,
+            _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true,
             _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
         };
         const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
@@ -184,10 +184,10 @@ export class CurrentUserUtils {
                 case DocumentType.IMG : creator = imageBox; break;
                 case DocumentType.FONTICON: creator = fontBox; break;
             }
-            const allopts = {system: true, ...opts};
+            const allopts = {system: true, onClickScriptDisable: "never",  ...opts};
             return DocUtils.AssignScripts(  (curIcon?.iconTemplate === opts.iconTemplate ? 
                       DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
-                {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)"});
+                {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", });
         };
         const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
             textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
@@ -314,7 +314,7 @@ export class CurrentUserUtils {
         const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => {
             const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined;
             const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, 
-                _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, 
+                _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, 
                 btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true,
                 _removeDropProperties: new List<string>(["_stayInCollection"]),
                 };
@@ -324,7 +324,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true,
             _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
-            childDocumentsActive: true, childDropAction: 'alias' 
+             childDropAction: 'alias' 
         };
         const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
         return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 0b92fd864..9fc1487a0 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -82,12 +82,6 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
             return this.props.fieldKey;
         }
 
-        isContentActive = (outsideReaction?: boolean) =>
-            this.props.isContentActive?.() === false
-                ? false
-                : Doc.ActiveTool !== 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;
@@ -135,13 +129,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
 
         isAnyChildContentActive = () => this._isAnyChildContentActive;
 
-        isContentActive = (outsideReaction?: boolean) =>
-            this.props.isContentActive?.() === false
-                ? false
-                : Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.rootSelected(outsideReaction) || this.isAnyChildContentActive()
-                ? true
-                : undefined;
-
         lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
 
         protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2d2d3c2f6..985e6f88f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -916,9 +916,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                         pointerEvents: 'none',
                                     }}>
                                     {this._isRotating ? null : (
-                                        <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
-                                            <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
-                                        </div>
+                                        <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}>
+                                            <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+                                                <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
+                                            </div>
+                                        </Tooltip>
                                     )}
                                 </div>
                                 {!this._showRotCenter ? null : (
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a085b69a5..e3642fdaf 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -473,7 +473,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
                             dontRegisterView={true}
                             noSidebar={true}
                             dontScale={true}
-                            isContentActive={this.isContentActive}
+                            isContentActive={this.props.isContentActive}
                         />
                     </div>
                 )}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c84d204d5..f5adc17d0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -40,7 +40,7 @@ import { DashboardView } from './DashboardView';
 import { DictationOverlay } from './DictationOverlay';
 import { DocumentDecorations } from './DocumentDecorations';
 import { GestureOverlay } from './GestureOverlay';
-import { DASHBOARD_SELECTOR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss';
+import { TOPBAR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss';
 import { Colors } from './global/globalEnums';
 import { KeyManager } from './GlobalKeyHandler';
 import { InkTranscription } from './InkTranscription';
@@ -86,7 +86,7 @@ export class MainView extends React.Component {
         return this._hideUI ? 0 : 27;
     } //  27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
     @computed private get topOfDashUI() {
-        return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
+        return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
     }
     @computed private get topOfHeaderBarDoc() {
         return this.topOfDashUI;
@@ -872,7 +872,7 @@ export class MainView extends React.Component {
 
     @computed get docButtons() {
         return !Doc.MyDockedBtns ? null : (
-            <div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !Doc.MyDockedBtns.linearViewIsExpanded ? '42px' : undefined }}>
+            <div className="mainView-docButtons" ref={this._docBtnRef}>
                 <CollectionLinearView
                     Document={Doc.MyDockedBtns}
                     DataDoc={undefined}
@@ -910,7 +910,7 @@ export class MainView extends React.Component {
         );
     }
     @computed get snapLines() {
-        return !SnappingManager.GetShowSnapLines() ? null : (
+        return !SelectionManager.Views().some(dv => dv.rootDoc.showSnapLines) ? null : (
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
                     {SnappingManager.horizSnapLines().map(l => (
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 1b5eb3342..faaa4e1f9 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -89,6 +89,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
     const isAnchor = property.includes(':anchor');
     const isAnnotated = property.includes(':annotated');
     const isOpen = property.includes(':open');
+    const boxBackground = property.includes(':box');
     const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : '';
     const lockedPosition = () => doc && BoolCast(doc._lockedPosition);
     const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
@@ -200,7 +201,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                     docColor = docColor || (darkScheme() ? 'transparent' : 'transparent');
                     break;
                 case DocumentType.FONTICON:
-                    docColor = docColor || Colors.DARK_GRAY;
+                    docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY;
                     break;
                 case DocumentType.RTF:
                     docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
@@ -289,6 +290,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         }
         case StyleProp.PointerEvents:
             const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
+            if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
             if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all';
             if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
             if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 33d468950..64ec419ec 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -325,6 +325,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 fitWidth={this.props.childFitWidth}
                 isContentActive={doc.isLinkButton ? this.isChildButtonContentActive : this.isChildContentActive}
                 onKey={this.onKeyDown}
+                onBrowseClick={this.props.onBrowseClick}
                 isDocumentActive={this.isContentActive}
                 LayoutTemplate={this.props.childLayoutTemplate}
                 LayoutTemplateString={this.props.childLayoutString}
@@ -659,6 +660,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         addDocument={this.props.addDocument}
                         moveDocument={this.props.moveDocument}
                         addDocTab={this.props.addDocTab}
+                        onBrowseClick={this.props.onBrowseClick}
                         pinToPres={emptyFunction}
                         rootSelected={this.props.isSelected}
                         removeDocument={this.props.removeDocument}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5e6849e7c..93b816581 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -10,7 +10,6 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { FieldId } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
 import { DocServer } from '../../DocServer';
@@ -28,6 +27,7 @@ import { LightboxView } from '../LightboxView';
 import { MainView } from '../MainView';
 import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 import { DashFieldView } from '../nodes/formattedText/DashFieldView';
+import { KeyValueBox } from '../nodes/KeyValueBox';
 import { PinProps, PresBox, PresMovement } from '../nodes/trails';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
 import { CollectionDockingView } from './CollectionDockingView';
@@ -35,7 +35,6 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
 import { CollectionView } from './CollectionView';
 import './TabDocView.scss';
 import React = require('react');
-import { KeyValueBox } from '../nodes/KeyValueBox';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 interface TabDocViewProps {
@@ -429,6 +428,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                     ContainingCollectionDoc={undefined}
                     onBrowseClick={MainView.Instance.exploreMode}
                     isContentActive={returnTrue}
+                    isDocumentActive={returnFalse}
                     PanelWidth={this.PanelWidth}
                     PanelHeight={this.PanelHeight}
                     styleProvider={DefaultStyleProvider}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 257428d56..99b7549c0 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -573,6 +573,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                         </div>
                     )}
                     <ul
+                        style={{ cursor: 'inherit' }}
                         key={expandKey + 'more'}
                         title="click to change sort order"
                         className={''} //this.doc.treeViewHideTitle ? 'no-indent' : ''}
@@ -629,7 +630,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             );
         } else if (this.treeViewExpandedView === 'fields') {
             return (
-                <ul key={this.doc[Id] + this.doc.title}>
+                <ul key={this.doc[Id] + this.doc.title} style={{ cursor: 'inherit' }}>
                     <div>{this.expandedField}</div>
                 </ul>
             );
@@ -903,7 +904,8 @@ export class TreeView extends React.Component<TreeViewProps> {
                 hideDecorationTitle={this.props.treeView.outlineMode}
                 hideResizeHandles={this.props.treeView.outlineMode}
                 styleProvider={this.titleStyleProvider}
-                onClickScriptDisable="never"
+                enableDragWhenActive={true}
+                onClickScriptDisable="never" // tree docViews have a script to show fields, etc.
                 docViewPath={returnEmptyDoclist}
                 treeViewDoc={this.props.treeView.props.Document}
                 addDocument={undefined}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8104ab1a7..aed3683d4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -821,9 +821,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false;
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             Doc.ActiveTool = InkTool.None;
-            if (this.props.isContentActive(true)) e.stopPropagation();
         } else {
-            this.props.DocumentView?.().docView?.cancelMoveEvents();
             if (this.tryDragCluster(e, this._hitCluster)) {
                 return true;
             }
@@ -955,96 +953,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
-    handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
-        if (!e.cancelBubble) {
-            const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-            if (myTouches[0]) {
-                if (Doc.ActiveTool === InkTool.None) {
-                    if (this.tryDragCluster(e, this._hitCluster)) {
-                        e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-                        e.preventDefault();
-                        document.removeEventListener('pointermove', this.onPointerMove);
-                        return;
-                    }
-                    // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected'
-                    this.pan(myTouches[0]);
-                }
-            }
-            // e.stopPropagation();
-            e.preventDefault();
-        }
-    };
-
-    handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
-        // pinch zooming
-        if (!e.cancelBubble) {
-            const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-            const pt1 = myTouches[0];
-            const pt2 = myTouches[1];
-
-            if (this.prevPoints.size === 2) {
-                const oldPoint1 = this.prevPoints.get(pt1.identifier);
-                const oldPoint2 = this.prevPoints.get(pt2.identifier);
-                if (oldPoint1 && oldPoint2) {
-                    const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
-
-                    // if zooming, zoom
-                    if (dir !== 0) {
-                        const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
-                        const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
-                        const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-                        const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-
-                        // calculate the raw delta value
-                        const rawDelta = dir * (d1 + d2);
-
-                        // this floors and ceils the delta value to prevent jitteriness
-                        const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8);
-                        this.zoom(centerX, centerY, delta * window.devicePixelRatio);
-                        this.prevPoints.set(pt1.identifier, pt1);
-                        this.prevPoints.set(pt2.identifier, pt2);
-                    }
-                    // this is not zooming. derive some form of panning from it.
-                    else {
-                        // use the centerx and centery as the "new mouse position"
-                        const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-                        const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-                        // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
-
-                        this._lastX = centerX;
-                        this._lastY = centerY;
-                    }
-                }
-            }
-            // e.stopPropagation();
-            e.preventDefault();
-        }
-    };
-
-    @action
-    handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
-        if (this.props.isContentActive(true)) {
-            // const pt1: React.Touch | null = e.targetTouches.item(0);
-            // const pt2: React.Touch | null = e.targetTouches.item(1);
-            // // if (!pt1 || !pt2) return;
-            const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-            const pt1 = myTouches[0];
-            const pt2 = myTouches[1];
-            if (pt1 && pt2) {
-                const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-                const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-                this._lastX = centerX;
-                this._lastY = centerY;
-
-                this.removeMoveListeners();
-                this.addMoveListeners();
-                this.removeEndListeners();
-                this.addEndListeners();
-                e.stopPropagation();
-            }
-        }
-    };
-
     cleanUpInteractions = () => {
         this.removeMoveListeners();
         this.removeEndListeners();
@@ -1637,7 +1545,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             } else {
                 const canvas = oldDiv;
                 const img = document.createElement('img'); // create a Image Element
-                img.src = canvas.toDataURL(); //image source
+                try {
+                    img.src = canvas.toDataURL(); //image source
+                } catch (e) {
+                    console.log(e);
+                }
                 img.style.width = canvas.style.width;
                 img.style.height = canvas.style.height;
                 const newCan = newDiv as HTMLCanvasElement;
@@ -2265,6 +2177,8 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
         const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
         ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
         Doc.linkFollowHighlight(dv?.props.Document, false);
+    } else {
+        DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true });
     }
 }
 ScriptingGlobals.add(CollectionBrowseClick);
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 521bcda1e..3e3709827 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -1,9 +1,13 @@
 @import '../../global/globalCssVariables';
 @import '../../_nodeModuleOverrides';
 
+.collectionLinearView {
+    width: 100%;
+}
 .collectionLinearView-label {
     color: black;
     background-color: $light-gray;
+    width: 100%;
 }
 .collectionLinearView-outer {
     overflow: visible;
@@ -15,8 +19,6 @@
     }
 
     &.true {
-        padding-left: 5px;
-        padding-right: 5px;
         border-left: $standard-border;
         background-color: $medium-blue-alt;
     }
@@ -29,7 +31,6 @@
         display: flex;
         height: 100%;
         align-items: center;
-        gap: 5px;
 
         .collectionView {
             overflow: visible !important;
@@ -101,13 +102,12 @@
             background-color: $medium-blue;
             padding: 5;
             border-radius: 2px;
-            height: 25;
+            height: 100%;
             min-width: 25;
             margin: 0;
             color: $white;
             display: flex;
             font-weight: 100;
-            width: fit-content;
             transition: transform 0.2s;
             align-items: center;
             justify-content: center;
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index d54e8ce98..a062c65fc 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { StylesProvider, Tooltip } from '@material-ui/core';
+import { Tooltip } from '@material-ui/core';
 import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -11,8 +11,6 @@ import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
-import { Colors, Shadows } from '../../global/globalEnums';
-import { media_state } from '../../nodes/AudioBox';
 import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
 import { DocumentView } from '../../nodes/DocumentView';
 import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
@@ -46,7 +44,7 @@ export class CollectionLinearView extends CollectionSubView() {
 
     componentDidMount() {
         this._widthDisposer = reaction(
-            () => 5 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * this.rootDoc[HeightSym]() : 10),
+            () => 5 + this.dimension() + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[WidthSym]() || this.dimension()) + tot + 4, 0) : 0),
             width => this.childDocs.length && (this.layoutDoc._width = width),
             { fireImmediately: true }
         );
@@ -180,10 +178,10 @@ export class CollectionLinearView extends CollectionSubView() {
                 ref={r => (dref = r || undefined)}
                 style={{
                     pointerEvents: 'all',
-                    width: nested ? undefined : NumCast(doc._width),
-                    height: nested ? undefined : NumCast(doc._height),
-                    marginLeft: !nested ? 2.5 : 0,
-                    marginRight: !nested ? 2.5 : 0,
+                    width: NumCast(doc._width),
+                    height: NumCast(doc._height),
+                    marginLeft: 2,
+                    marginRight: 2,
                     // width: NumCast(pair.layout._width),
                     // height: NumCast(pair.layout._height),
                 }}>
@@ -199,7 +197,7 @@ export class CollectionLinearView extends CollectionSubView() {
                     rootSelected={this.props.isSelected}
                     removeDocument={this.props.removeDocument}
                     ScreenToLocalTransform={docXf}
-                    PanelWidth={nested ? doc[WidthSym] : this.dimension}
+                    PanelWidth={doc[WidthSym]}
                     PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension}
                     renderDepth={this.props.renderDepth + 1}
                     dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)}
@@ -236,7 +234,7 @@ export class CollectionLinearView extends CollectionSubView() {
 
         return (
             <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}>
-                <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu}>
+                <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension() }}>
                     {!this.props.Document.linearViewExpandable ? null : (
                         <Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top">
                             {menuOpener}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 34e591195..1ef2fb4ef 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -8,7 +8,8 @@
 
     .schema-table {
         background-color: $white;
-        cursor: default;
+        cursor: grab;
+        overflow: scroll;
 
         .schema-column-menu,
         .schema-filter-menu {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 60202a19e..d47c9762c 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -9,7 +9,7 @@ import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
-import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
+import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
@@ -46,7 +46,7 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     public static _rowHeight: number = 40;
     public static _minColWidth: number = 25;
-    public static _rowMenuWidth: number = 100;
+    public static _rowMenuWidth: number = 60;
     public static _previewDividerWidth: number = 4;
 
     @computed get _selectedDocs() {
@@ -364,7 +364,11 @@ export class CollectionSchemaView extends CollectionSubView() {
             this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...removed, ...draggedDocs, ...pushedDocs]);
             this.setSort(undefined);
             SelectionManager.DeselectAll();
-            setTimeout(() => draggedDocs.forEach(doc => DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true))), 100);
+            draggedDocs.forEach(doc => {
+                const draggedView = DocumentManager.Instance.getFirstDocumentView(doc);
+                if (draggedView) DocumentManager.Instance.RemoveView(draggedView);
+                DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true));
+            });
             e.stopPropagation();
             return true;
         }
@@ -812,26 +816,17 @@ export class CollectionSchemaView extends CollectionSubView() {
     previewWidthFunc = () => this.previewWidth;
     render() {
         return (
-            <div
-                className="collectionSchemaView"
-                ref={(ele: HTMLDivElement | null) => {
-                    this.createDashEventsTarget(ele);
-                }}
-                onPointerDown={e => {
-                    // this is analogous to the panning code for a freeform view.
-                    // however, schema views don't pan so it does nothing.  but it does eat the pointerDown event
-                    // if the content is active to prevent the schema from being dragged
-                    this.isContentActive() && setupMoveUpEvents(this, e, returnFalse, emptyFunction, emptyFunction, false);
-                }}
-                onDrop={this.onExternalDrop.bind(this)}>
-                <div className="schema-table">
+            <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}>
+                <div
+                    className="schema-table"
+                    onWheel={e => this.props.isContentActive() && e.stopPropagation()}
+                    ref={r => {
+                        // prevent wheel events from passively propagating up through containers
+                        r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false });
+                    }}>
                     <div className="schema-header-row" style={{ height: CollectionSchemaView._rowHeight }}>
                         <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
-                            <div
-                                className="schema-header-button"
-                                onPointerDown={e => {
-                                    this._columnMenuIndex && this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true);
-                                }}>
+                            <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}>
                                 <FontAwesomeIcon icon="plus" />
                             </div>
                         </div>
@@ -867,8 +862,9 @@ export class CollectionSchemaView extends CollectionSubView() {
                                 DataDoc={undefined}
                                 fitContentsToBox={returnTrue}
                                 dontCenter={'y'}
+                                onClickScriptDisable="always"
                                 focus={DocUtils.DefaultFocus}
-                                renderDepth={this.props.renderDepth}
+                                renderDepth={this.props.renderDepth + 1}
                                 rootSelected={this.rootSelected}
                                 PanelWidth={this.previewWidthFunc}
                                 PanelHeight={this.props.PanelHeight}
@@ -915,16 +911,22 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
                     return (
                         <div className="schema-row-wrapper" style={{ maxHeight: CollectionSchemaView._rowHeight }}>
                             <DocumentView
+                                key={doc[Id]}
                                 {...this.props.schema.props}
                                 LayoutTemplate={this.props.schema.props.childLayoutTemplate}
                                 LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)}
                                 Document={doc}
                                 DataDoc={dataDoc}
+                                renderDepth={this.props.schema.props.renderDepth + 1}
                                 ContainingCollectionView={this.props.schema.props.CollectionView}
                                 ContainingCollectionDoc={this.props.schema.Document}
                                 PanelWidth={this.tableWidthFunc}
                                 PanelHeight={this.rowHeightFunc}
                                 styleProvider={DefaultStyleProvider}
+                                waitForDoubleClickToClick={returnNever}
+                                defaultDoubleClick={returnDefault}
+                                enableDragWhenActive={true}
+                                onClickScriptDisable="always"
                                 focus={this.props.schema.focusDocument}
                                 docFilters={this.props.schema.childDocFilters}
                                 docRangeFilters={this.props.schema.childDocRangeFilters}
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 10f307068..2defaae00 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -88,11 +88,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return (
             <div
                 className="schema-row"
-                style={
-                    this.props.isSelected()
-                        ? { height: CollectionSchemaView._rowHeight, backgroundColor: Colors.LIGHT_BLUE, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined /*, opacity: this.props.dragging ? 0.5 : 1 */ }
-                        : { height: CollectionSchemaView._rowHeight, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined }
-                }
+                style={{ height: CollectionSchemaView._rowHeight, backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined }}
                 onPointerEnter={this.onPointerEnter}
                 onPointerLeave={this.onPointerLeave}
                 ref={(row: HTMLDivElement | null) => {
@@ -103,7 +99,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     className="row-menu"
                     style={{
                         width: CollectionSchemaView._rowMenuWidth,
-                        pointerEvents: !this.isContentActive() ? 'none' : undefined,
+                        pointerEvents: !this.props.isContentActive() ? 'none' : undefined,
                     }}>
                     <div
                         className="schema-row-button"
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 430a36dce..3496bb835 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -59,7 +59,6 @@ $standard-border-radius: 3px;
 // shadow
 $standard-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
 
-$dashboardselector-height: 32px;
 $mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
 $docDecorations-zindex: 998; // then doc decorations appear over everything else
 $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
@@ -79,7 +78,7 @@ $TREE_BULLET_WIDTH: 20px;
     MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
     SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
     ANTIMODEMENU_HEIGHT: $antimodemenu-height;
-    DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height;
+    TOPBAR_HEIGHT: $topbar-height;
     DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM;
     LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
     TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts
index 3375579d6..537ea1e5d 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -6,7 +6,7 @@ interface IGlobalScss {
     MAX_ROW_HEIGHT: string;
     SEARCH_THUMBNAIL_SIZE: string;
     ANTIMODEMENU_HEIGHT: string;
-    DASHBOARD_SELECTOR_HEIGHT: string;
+    TOPBAR_HEIGHT: string;
     DFLT_IMAGE_NATIVE_DIM: string;
     LEFT_MENU_WIDTH: string;
     TREE_BULLET_WIDTH: string;
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index c229a966a..70ba7e182 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -51,7 +51,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const scaling = Math.min(this.layoutDoc.fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]());
         return (
             <div
-                className={`colorBox-container${this.isContentActive() ? '-interactive' : ''}`}
+                className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`}
                 onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
                 onClick={e => e.stopPropagation()}
                 style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 541dddd83..9a3e77e6e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -5,7 +5,6 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from 'mobx-react';
 import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
 import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
-import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
@@ -15,7 +14,7 @@ import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnNone, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -50,11 +49,9 @@ import { FieldViewProps } from './FieldView';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { RadialMenu } from './RadialMenu';
-import { ScriptingBox } from './ScriptingBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
-import { KeyValueBox } from './KeyValueBox';
 const { Howl } = require('howler');
 
 interface Window {
@@ -135,6 +132,7 @@ export interface DocComponentView {
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
     incrementalRendering?: () => void;
     fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox)
+    overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
@@ -195,6 +193,9 @@ export interface DocumentViewSharedProps {
     forceAutoHeight?: boolean;
     disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
     onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
+    enableDragWhenActive?: boolean;
+    waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
+    defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
     pointerEvents?: () => Opt<string>;
     scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
     createNewFilterDoc?: () => void;
@@ -255,8 +256,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private _downX: number = 0;
     private _downY: number = 0;
     private _downTime: number = 0;
-    private _firstX: number = -1;
-    private _firstY: number = -1;
     private _lastTap: number = 0;
     private _doubleTap = false;
     private _mainCont = React.createRef<HTMLDivElement>();
@@ -333,11 +332,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         return this.props.NativeHeight();
     }
     @computed get disableClickScriptFunc() {
+        const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable;
+        // prettier-ignore
         return (
             DocumentView.LongPress ||
-            this.props.onClickScriptDisable === 'always' ||
-            this._componentView?.onClickScriptDisable?.() ||
-            (this.props.onClickScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) ||
+            onScriptDisable === 'always' ||
+            (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) ||
             this._componentView?.isAnyChildContentActive?.()
         );
     }
@@ -360,7 +360,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     componentDidMount() {
         this.setupHandlers();
     }
-    //componentDidUpdate() { this.setupHandlers(); }
+
     setupHandlers() {
         this.cleanupHandlers(false);
         if (this._mainCont.current) {
@@ -378,151 +378,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
-    handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
-        this.removeMoveListeners();
-        this.removeEndListeners();
-        document.removeEventListener('pointermove', this.onPointerMove);
-        document.removeEventListener('pointerup', this.onPointerUp);
-        if (RadialMenu.Instance._display === false) {
-            this.addHoldMoveListeners();
-            this.addHoldEndListeners();
-            this.onRadialMenu(e, me);
-            const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
-            this._firstX = pt.pageX;
-            this._firstY = pt.pageY;
-        }
-    };
-
-    handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-        const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
-
-        if (this._firstX === -1 || this._firstY === -1) {
-            return;
-        }
-        if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
-            this.handle1PointerHoldEnd(e, me);
-        }
-    };
-
-    handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
-        this.removeHoldMoveListeners();
-        this.removeHoldEndListeners();
-        RadialMenu.Instance.closeMenu();
-        this._firstX = -1;
-        this._firstY = -1;
-        SelectionManager.DeselectAll();
-        me.touchEvent.stopPropagation();
-        me.touchEvent.preventDefault();
-        e.stopPropagation();
-        if (RadialMenu.Instance.used) {
-            this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY);
-        }
-    };
-
-    handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
-        if (!this.props.isSelected()) {
-            e.stopPropagation();
-            e.preventDefault();
-
-            this.removeMoveListeners();
-            this.addMoveListeners();
-            this.removeEndListeners();
-            this.addEndListeners();
-        }
-    };
-
-    handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
-        SelectionManager.DeselectAll();
-        if (this.Document.onPointerDown) return;
-        const touch = me.touchEvent.changedTouches.item(0);
-        if (touch) {
-            this._downX = touch.clientX;
-            this._downY = touch.clientY;
-            if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
-                e.stopPropagation();
-            }
-            this.removeMoveListeners();
-            this.addMoveListeners();
-            this.removeEndListeners();
-            this.addEndListeners();
-            e.stopPropagation();
-        }
-    };
-
-    handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
-        if (e.cancelBubble && this.props.isDocumentActive?.()) {
-            this.removeMoveListeners();
-        } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
-            const touch = me.touchEvent.changedTouches.item(0);
-            if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
-                if (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) {
-                    this.cleanUpInteractions();
-                    this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined);
-                }
-            }
-            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-            e.preventDefault();
-        }
-    };
-
-    @action
-    handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
-        const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
-        const pt1 = myTouches[0];
-        const pt2 = myTouches[1];
-        const oldPoint1 = this.prevPoints.get(pt1.identifier);
-        const oldPoint2 = this.prevPoints.get(pt2.identifier);
-        const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
-        if (pinching !== 0 && oldPoint1 && oldPoint2) {
-            const dW = Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX);
-            const dH = Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY);
-            const dX = -1 * Math.sign(dW);
-            const dY = -1 * Math.sign(dH);
-
-            if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
-                const doc = Document(this.props.Document);
-                const layoutDoc = Document(Doc.Layout(this.props.Document));
-                let nwidth = Doc.NativeWidth(layoutDoc);
-                let nheight = Doc.NativeHeight(layoutDoc);
-                const width = layoutDoc._width || 0;
-                const height = layoutDoc._height || (nheight / nwidth) * width;
-                const scale = this.props.ScreenToLocalTransform().Scale * this.NativeDimScaling;
-                const actualdW = Math.max(width + dW * scale, 20);
-                const actualdH = Math.max(height + dH * scale, 20);
-                doc.x = (doc.x || 0) + dX * (actualdW - width);
-                doc.y = (doc.y || 0) + dY * (actualdH - height);
-                const fixedAspect = e.ctrlKey || (nwidth && nheight);
-                if (fixedAspect && (!nwidth || !nheight)) {
-                    Doc.SetNativeWidth(layoutDoc, (nwidth = layoutDoc._width || 0));
-                    Doc.SetNativeHeight(layoutDoc, (nheight = layoutDoc._height || 0));
-                }
-                if (nwidth > 0 && nheight > 0) {
-                    if (Math.abs(dW) > Math.abs(dH)) {
-                        if (!fixedAspect) {
-                            Doc.SetNativeWidth(layoutDoc, (actualdW / (layoutDoc._width || 1)) * Doc.NativeWidth(layoutDoc));
-                        }
-                        layoutDoc._width = actualdW;
-                        if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width;
-                        else layoutDoc._height = actualdH;
-                    } else {
-                        if (!fixedAspect) {
-                            Doc.SetNativeHeight(layoutDoc, (actualdH / (layoutDoc._height || 1)) * Doc.NativeHeight(doc));
-                        }
-                        layoutDoc._height = actualdH;
-                        if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height;
-                        else layoutDoc._width = actualdW;
-                    }
-                } else {
-                    dW && (layoutDoc._width = actualdW);
-                    dH && (layoutDoc._height = actualdH);
-                    dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
-                }
-            }
-            e.stopPropagation();
-            e.preventDefault();
-        }
-    };
-
     @action
     onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
         const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
@@ -591,11 +446,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     };
 
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
-        if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+        if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
             let stopPropagate = true;
             let preventDefault = true;
             (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
             if (this._doubleTap) {
+                const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick;
                 if (this.onDoubleClickHandler?.script) {
                     const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables
                     // prettier-ignore
@@ -609,7 +465,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                             value: undefined,
                         }, console.log  );
                     UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
-                } else if (!Doc.IsSystem(this.rootDoc) && (this.Document.defaultDoubleClick === undefined || this.Document.defaultDoubleClick === 'default')) {
+                } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) {
                     UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
@@ -649,15 +505,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         LinkFollower.FollowLink(undefined, this.Document, e.altKey);
                     };
                 } else {
+                    // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                     if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
-                        // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                         stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
                     }
                     preventDefault = false;
                 }
 
                 this._singleClickFunc = clickFunc ?? (() => (this._componentView?.select ?? this.props.select)(e.ctrlKey || e.metaKey, e.shiftKey));
-                if ((clickFunc && this.Document.waitForDoubleClickToClick !== 'never') || this.Document.waitForDoubleClickToClick === 'always') {
+                const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
+                if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
                     this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
                     this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
                 } else {
@@ -673,55 +530,46 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000);
-        if (!(e.nativeEvent as any).DownDocView) (e.nativeEvent as any).DownDocView = GestureOverlay.DownDocView = this.props.DocumentView();
-        if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
-        // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
-        if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) {
-            if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
-                e.stopPropagation();
-                if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
-                // TODO: check here for panning/inking
-            }
-            return;
-        }
+        if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
+
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._downTime = Date.now();
         if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
-            // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
+            // click events stop here if the document is active and no modes are overriding it
+            // if this is part of a template, let the event go up to the template root unless right/ctrl clicking
             if (
-                (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+                // prettier-ignore
+                this.props.isDocumentActive?.() &&
                 !this.props.onBrowseClick?.() &&
                 !this.Document.ignoreClick &&
                 !e.ctrlKey &&
-                (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
+                e.button === 0 &&
+                this.pointerEvents !== 'none' &&
                 !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
             ) {
                 e.stopPropagation();
                 // don't preventDefault anymore.  Goldenlayout, PDF text selection and RTF text selection all need it to go though
                 //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
+
+                // listen to move events if document content isn't active or document is draggable
+                if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) {
+                    document.addEventListener('pointermove', this.onPointerMove);
+                }
             }
-            if (this.props.isDocumentActive?.()) {
-                document.removeEventListener('pointermove', this.onPointerMove);
-                document.addEventListener('pointermove', this.onPointerMove);
-            }
-            document.removeEventListener('pointerup', this.onPointerUp);
             document.addEventListener('pointerup', this.onPointerUp);
         }
     };
 
     @action
     onPointerMove = (e: PointerEvent): void => {
-        if (this.layoutDoc._lockedPosition || DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) return;
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
 
-        if (((!this.topMost && this.props.isDocumentActive?.()) || this.layoutDoc.onDragStart) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
+        if (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
                 this.cleanupPointerEvents();
                 this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
             }
-            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-            e.preventDefault();
         }
     };
 
@@ -738,13 +586,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         this.cleanupPointerEvents();
         this._longPressSelector && clearTimeout(this._longPressSelector);
 
-        const now = Date.now();
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
-        } else if (now - this._downTime < 300) {
-            this._doubleTap = now - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
-            // bcz: this is a placeholder.  documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
-            if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
+        } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+            this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME;
+            if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
         if (DocumentView.LongPress) e.preventDefault();
     };
@@ -1386,11 +1232,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             </div>
         );
     }
-    @observable _: string = '';
+
     renderDoc = (style: object) => {
         TraceMobx();
         const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
-        const isButton = this.props.Document.type === DocumentType.FONTICON;
+        const background = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
         if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
         return (
             this.docContents ?? (
@@ -1399,7 +1245,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     id={this.props.Document[Id]}
                     style={{
                         ...style,
-                        background: isButton || thumb ? undefined : this.backgroundColor,
+                        background: thumb ? undefined : background,
                         opacity: this.opacity,
                         cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
                         color: StrCast(this.layoutDoc.color, 'inherit'),
@@ -1809,7 +1655,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         TraceMobx();
         const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
         const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
-        const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
 
         return this.hidden ? null : (
             <div
@@ -1831,12 +1676,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         ref={this.ContentRef}
                         style={{
                             transition: this.props.dataTransition,
-                            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 || this.props.LayoutTemplateString?.includes(KeyValueBox.name) || this.props.forceAutoHeight
-                                    ? undefined
-                                    : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
+                            transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
+                            width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
+                            height: 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/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 75b4907cd..b43e359ff 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -101,7 +101,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
             <div
                 ref={this.createDropTarget}
                 style={{
-                    pointerEvents: !this.isContentActive() ? 'all' : undefined,
+                    pointerEvents: !this.props.isContentActive() ? 'all' : undefined,
                     width: this.props.PanelWidth(),
                     height: this.props.PanelHeight(),
                 }}>
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 9c31ed3e4..57018fb93 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -4,8 +4,9 @@ import { Doc, Field, FieldResult } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DocCast, NumCast } from '../../../fields/Types';
+import { DocCast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
+import { returnAll, returnAlways, returnTrue } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
 import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting';
@@ -20,7 +21,6 @@ import './KeyValueBox.scss';
 import { KeyValuePair } from './KeyValuePair';
 import React = require('react');
 import e = require('express');
-import { returnTrue } from '../../../Utils';
 
 export type KVPScript = {
     script: CompiledScript;
@@ -42,8 +42,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     componentDidMount() {
         this.props.setContentView?.(this);
     }
-    onClickScriptDisable: () => 'always' = () => 'always';
+    reverseNativeScaling = returnTrue;
+    able = returnAlways;
     fitWidth = returnTrue;
+    overridePointerEvents = returnAll;
+    onClickScriptDisable = returnAlways;
 
     @observable private rows: KeyValuePair[] = [];
 
@@ -69,9 +72,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     };
     public static CompileKVPScript(value: string): KVPScript | undefined {
         const eq = value.startsWith('=');
-        value = eq ? value.substr(1) : value;
-        const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false;
-        value = dubEq ? value.substr(2) : value;
+        value = eq ? value.substring(1) : value;
+        const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
+        value = dubEq ? value.substring(2) : value;
         const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false };
         if (dubEq) options.typecheck = false;
         const script = CompileScript(value, options);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index c4adc7f1a..94434dce7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,19 +1,18 @@
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, Field, Opt } from '../../../fields/Doc';
-import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Doc, Field } from '../../../fields/Doc';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { EditableView } from '../EditableView';
+import { DefaultStyleProvider } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from './KeyValueBox';
 import './KeyValueBox.scss';
 import './KeyValuePair.scss';
 import React = require('react');
-import { DefaultStyleProvider } from '../StyleProvider';
-import { OpenWhere } from './DocumentView';
 
 // Represents one row in a key value plane
 
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 470f7e803..46ccdecae 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,6 +1,6 @@
 import React = require('react');
 import { observer } from 'mobx-react';
-import { emptyFunction, returnFalse, returnTrue } from '../../../Utils';
+import { emptyFunction, returnAlways, returnFalse, returnTrue } from '../../../Utils';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import { ComparisonBox } from './ComparisonBox';
@@ -12,23 +12,22 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(LinkBox, fieldKey);
     }
-    isContentActiveFunc = () => this.isContentActive();
 
-    onClickScriptDisable: () => 'always' = () => 'always';
+    onClickScriptDisable = returnAlways;
     componentDidMount() {
         this.props.setContentView?.(this);
     }
     render() {
         if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true));
         return (
-            <div className={`linkBox-container${this.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
+            <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
                 <ComparisonBox
                     {...this.props}
                     fieldKey="anchor"
                     setHeight={emptyFunction}
                     dontRegisterView={true}
                     renderDepth={this.props.renderDepth + 1}
-                    isContentActive={this.isContentActiveFunc}
+                    isContentActive={this.props.isContentActive}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
                     moveDocument={returnFalse}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 5683b0fe7..f09962b22 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -8,7 +8,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { returnEmptyString, returnTrue } from '../../../Utils';
+import { returnAlways, returnEmptyString, returnTrue } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { InteractionUtils } from '../../util/InteractionUtils';
 import { CompileScript, ScriptParam } from '../../util/Scripting';
@@ -114,7 +114,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
         }
     }
 
-    onClickScriptDisable: () => 'always' = () => 'always';
+    onClickScriptDisable = returnAlways;
 
     @action
     componentDidMount() {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 73283263f..d57518a8d 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -876,7 +876,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
     };
     @computed get content() {
-        const interactive = this.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
+        const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
         return (
             <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
                 {this.urlContent}
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
index 7fe1436c7..f3b43501b 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -158,7 +158,7 @@
         width: 100%;
         border-radius: 100%;
         flex-direction: column;
-        margin-top: -4px;
+        // margin-top: -4px;
 
         svg {
             width: 60% !important;
@@ -427,11 +427,11 @@
     }
 
     .dropbox-background {
-        width: 100vw;
-        height: 100vh;
-        top: 0;
+        width: 200vw;
+        height: 200vh;
+        top: -100vh;
         z-index: 20;
-        left: 0;
+        left: -100vw;
         background: transparent;
         position: fixed;
     }
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 468bcc4d8..28e6eaf1c 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -618,11 +618,10 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
     const selected = SelectionManager.Docs().lastElement();
     // prettier-ignore
-    const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void; setMode?: () => void }> = new Map([
+    const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void;}> = new Map([
         ['font', {
             checkResult: () => RichTextMenu.Instance?.fontFamily,
             setDoc: () => value && RichTextMenu.Instance.setFontFamily(value),
-            setMode: () => Doc.UserDoc().textAlign = value,
         }],
         ['highlight', {
             checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight,
@@ -645,8 +644,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     if (checkResult) {
         return map.get(attr)?.checkResult();
     }
-    if (editorView?.state) map.get(attr)?.setDoc();
-    else map.get(attr)?.setMode?.();
+    map.get(attr)?.setDoc?.();
 });
 
 type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3ce2366f8..ee4249b02 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1470,20 +1470,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 //      but that's changed, so this shouldn't be needed.
                 //e.stopPropagation();  // if the text box is selected, then it consumes all down events
                 document.addEventListener('pointerup', this.onSelectEnd);
-                document.addEventListener('pointermove', this.onSelectMove);
             }
         }
         if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
             e.preventDefault();
         }
     };
-    onSelectMove = (e: PointerEvent) => {
-        this.props.DocumentView?.().docView?.cancelMoveEvents();
-        e.stopPropagation();
-    };
     onSelectEnd = (e: PointerEvent) => {
         document.removeEventListener('pointerup', this.onSelectEnd);
-        document.removeEventListener('pointermove', this.onSelectMove);
     };
     onPointerUp = (e: React.PointerEvent): void => {
         const editor = this._editorView!;
@@ -1534,6 +1528,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     @observable public static Focused: FormattedTextBox | undefined;
 
     onClick = (e: React.MouseEvent): void => {
+        if (!this.props.isContentActive()) return;
         if ((e.nativeEvent as any).handledByInnerReactInstance) {
             e.stopPropagation();
             return;
@@ -1876,7 +1871,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     r?.addEventListener(
                         'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
                         (e: WheelEvent) => {
-                            if (this.isContentActive()) {
+                            if (this.props.isContentActive()) {
                                 if (!NumCast(this.layoutDoc._scrollTop) && e.deltaY <= 0) e.preventDefault();
                                 e.stopPropagation();
                             }
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e79e7472a..3376c29a9 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1027,8 +1027,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
     getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
     panelHeight = () => this.props.PanelHeight() - 40;
-    isContentActive = (outsideReaction?: boolean) =>
-        Doc.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
+    isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(outsideReaction);
+    //.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
 
     /**
      * For sorting the array so that the order is maintained when it is dropped.
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index eb3087399..c5060a2c2 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -381,7 +381,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
             const target = e.target as any;
             if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
                 this._textSelecting = false;
-                document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called
             } else {
                 // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee.
                 setTimeout(
@@ -391,7 +390,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
                 this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, 'htmlAnnotation', { 'pointer-events': 'none' });
                 document.addEventListener('pointerup', this.onSelectEnd);
-                document.addEventListener('pointermove', this.onSelectMove);
             }
         }
     };
@@ -401,12 +399,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
         this.isAnnotating = false;
         this._marqueeing = undefined;
         this._textSelecting = true;
-        document.removeEventListener('pointermove', this.onSelectMove);
-    };
-
-    onSelectMove = (e: PointerEvent) => {
-        this.props.DocumentView?.().docView?.cancelMoveEvents();
-        e.stopPropagation();
     };
 
     @action
@@ -414,7 +406,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
         this.isAnnotating = false;
         clearStyleSheetRules(PDFViewer._annotationStyle);
         this.props.select(false);
-        document.removeEventListener('pointermove', this.onSelectMove);
         document.removeEventListener('pointerup', this.onSelectEnd);
 
         const sel = window.getSelection();
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 4d82551db..553c4525c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -31,7 +31,7 @@ export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
         const onDelegate = Object.keys(doc).includes(key);
         const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-        return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
+        return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field));
     }
     export function toScriptString(field: Field): string {
         switch (typeof field) {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index feb419597..2b8750714 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -114,7 +114,7 @@ export class ScriptField extends ObjectField {
     }
 
     [ToScriptString]() {
-        return 'script field';
+        return this.script.originalScript;
     }
     [ToString]() {
         return this.script.originalScript;
-- 
cgit v1.2.3-70-g09d2


From c5c2c309cd88bbeb2f1b668cb040cffaac9c8470 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 4 Apr 2023 20:12:16 -0400
Subject: fixed using freeformview in sidebar of pdfs. fixed issues with
 dragging items out of schema and with selecting the schema view by clicking
 on headers.  Fixed a lot of errors caused by using OmitKeys which masks type
 checking.  fixed some pointerevent problems with treeview and freeformview

---
 src/client/documents/Documents.ts                  |  22 ++--
 src/client/views/DocumentDecorations.tsx           |  40 +++---
 src/client/views/EditableView.tsx                  |   4 +-
 src/client/views/InkStrokeProperties.ts            |   1 -
 src/client/views/InkingStroke.tsx                  |  13 +-
 src/client/views/MainView.tsx                      |   1 +
 src/client/views/PropertiesView.tsx                |  73 ++---------
 src/client/views/SidebarAnnos.tsx                  |  15 +--
 src/client/views/StyleProvider.tsx                 |   2 +-
 src/client/views/TemplateMenu.tsx                  |   2 +-
 .../views/collections/CollectionCarousel3DView.tsx |   7 +-
 .../views/collections/CollectionCarouselView.tsx   |   8 +-
 .../collections/CollectionStackedTimeline.tsx      |  54 +++-----
 .../views/collections/CollectionTreeView.tsx       |  24 ++--
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 139 +++++++++++---------
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +
 .../collectionGrid/CollectionGridView.tsx          |   9 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  99 +++------------
 .../collectionSchema/SchemaColumnHeader.tsx        |  10 +-
 src/client/views/nodes/AudioBox.tsx                |   6 +-
 src/client/views/nodes/ComparisonBox.tsx           |   6 +-
 src/client/views/nodes/DocumentView.tsx            |  26 +---
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  12 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |   6 +-
 src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx |   5 +-
 src/client/views/nodes/PDFBox.tsx                  | 141 ++++++++++++++++-----
 src/client/views/nodes/ScreenshotBox.tsx           |  18 ++-
 src/client/views/nodes/VideoBox.tsx                |  12 +-
 src/client/views/nodes/WebBox.tsx                  |  14 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  27 ++--
 src/client/views/pdf/PDFViewer.tsx                 |  15 ++-
 src/fields/Doc.ts                                  |   9 +-
 34 files changed, 389 insertions(+), 437 deletions(-)

(limited to 'src/client/views/DocumentDecorations.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9b8b9c877..9cb480c4a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1420,41 +1420,39 @@ export namespace DocUtils {
 
     export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
         let created: Doc | undefined;
-        let layout: ((fieldKey: string) => string) | undefined;
         const field = target[fieldKey];
-        const resolved = options || {};
+        const resolved = options ?? {};
         if (field instanceof ImageField) {
             created = Docs.Create.ImageDocument(field.url.href, resolved);
-            layout = ImageBox.LayoutString;
+            created.layout = ImageBox.LayoutString(fieldKey);
         } else if (field instanceof Doc) {
             created = field;
         } else if (field instanceof VideoField) {
             created = Docs.Create.VideoDocument(field.url.href, resolved);
-            layout = VideoBox.LayoutString;
+            created.layout = VideoBox.LayoutString(fieldKey);
         } else if (field instanceof PdfField) {
             created = Docs.Create.PdfDocument(field.url.href, resolved);
-            layout = PDFBox.LayoutString;
+            created.layout = PDFBox.LayoutString(fieldKey);
         } else if (field instanceof AudioField) {
             created = Docs.Create.AudioDocument(field.url.href, resolved);
-            layout = AudioBox.LayoutString;
+            created.layout = AudioBox.LayoutString(fieldKey);
         } else if (field instanceof RecordingField) {
             created = Docs.Create.RecordingDocument(field.url.href, resolved);
-            layout = RecordingBox.LayoutString;
+            created.layout = RecordingBox.LayoutString(fieldKey);
         } else if (field instanceof InkField) {
             created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved);
-            layout = InkingStroke.LayoutString;
+            created.layout = InkingStroke.LayoutString(fieldKey);
         } else if (field instanceof List && field[0] instanceof Doc) {
             created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-            layout = CollectionView.LayoutString;
+            created.layout = CollectionView.LayoutString(fieldKey);
         } else if (field instanceof MapField) {
             created = Docs.Create.MapDocument(DocListCast(field), resolved);
-            layout = MapBox.LayoutString;
+            created.layout = MapBox.LayoutString(fieldKey);
         } else {
             created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
-            layout = FormattedTextBox.LayoutString;
+            created.layout = FormattedTextBox.LayoutString(fieldKey);
         }
         if (created) {
-            created.layout = layout?.(fieldKey);
             created.title = fieldKey;
             proto && created.proto && (created.proto = Doc.GetProto(proto));
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 985e6f88f..9bc583ce5 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -31,6 +31,7 @@ import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
+import { RichTextField } from '../../fields/RichTextField';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -127,19 +128,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         }
                         //@ts-ignore
                         const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
-                        Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
 
-                        if (d.rootDoc.syncLayoutFieldWithTitle) {
-                            const title = titleField.toString();
+                        if (titleField.toString().startsWith('<this>')) {
+                            const title = titleField.toString().replace(/<this>\.?/, '');
                             const curKey = Doc.LayoutFieldKey(d.rootDoc);
-                            if (curKey !== title && d.dataDoc[title] === undefined) {
-                                d.rootDoc.layout = FormattedTextBox.LayoutString(title);
-                                setTimeout(() => {
-                                    const val = d.dataDoc[curKey];
-                                    d.dataDoc[curKey] = undefined;
-                                    d.dataDoc[title] = val;
-                                });
+                            if (curKey !== title) {
+                                if (title) {
+                                    if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') {
+                                        d.rootDoc.layoutKey = `layout_${title}`;
+                                        d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title);
+                                        d.rootDoc[`${title}-nativeWidth`] = d.rootDoc[`${title}-nativeHeight`] = 0;
+                                    }
+                                } else {
+                                    d.rootDoc.layoutKey = undefined;
+                                }
                             }
+                        } else {
+                            Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
                         }
                     }),
                 'title blur'
@@ -453,17 +458,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        const views = SelectionManager.Views().map(dv => dv.rootDoc);
-        this._inkDragDocs = views
-            .filter(doc => doc.type === DocumentType.INK)
-            .map(doc => {
-                if (InkStrokeProperties.Instance._lock) {
-                    Doc.SetNativeHeight(doc, NumCast(doc._height));
-                    Doc.SetNativeWidth(doc, NumCast(doc._width));
-                }
-                return { doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) };
-            });
-
         setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
         this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
         this._resizeHdlId = e.currentTarget.className;
@@ -487,10 +481,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         if (!first) return false;
         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)
-                .forEach(dv => (fixedAspect = Doc.NativeAspect(dv.rootDoc)));
 
         const resizeHdl = this._resizeHdlId.split(' ')[0];
         if (fixedAspect && (resizeHdl === 'documentDecorations-bottomRightResizer' || resizeHdl === 'documentDecorations-topLeftResizer')) {
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index bb190e93b..164b6c57a 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -129,7 +129,7 @@ export class EditableView extends React.Component<EditableProps> {
                 this._editing = true;
                 this.props.isEditingCallback?.(true);
             }
-            e.stopPropagation();
+            // e.stopPropagation();
         }
     };
 
@@ -215,7 +215,7 @@ export class EditableView extends React.Component<EditableProps> {
                 className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`}
                 ref={this._ref}
                 style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }}
-                onPointerDown={e => e.stopPropagation()}
+                //onPointerDown={this.stopPropagation}
                 onClick={this.onClick}
                 placeholder={this.props.placeholder}>
                 <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 1d8d52425..e6df0801c 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -19,7 +19,6 @@ export class InkStrokeProperties {
         return this._Instance || new InkStrokeProperties();
     }
 
-    @observable _lock = false;
     @observable _controlButton = false;
     @observable _currentPoint = -1;
 
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index e3642fdaf..2fd6cc4d6 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,10 +25,11 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
 import { InkData, InkField } from '../../fields/InkField';
-import { BoolCast, Cast, DocCast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
-import { DashColor, OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { DashColor, returnFalse, setupMoveUpEvents } from '../../Utils';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
+import { Docs } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { Transform } from '../util/Transform';
@@ -41,13 +42,12 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
-import { DocComponentView, DocFocusOptions, DocumentView } from './nodes/DocumentView';
+import { DocComponentView } from './nodes/DocumentView';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { PinProps, PresBox } from './nodes/trails';
 import { StyleProp } from './StyleProvider';
 import Color = require('color');
-import { Docs } from '../documents/Documents';
-import { PinProps, PresBox } from './nodes/trails';
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -464,7 +464,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
                             //top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
                         }}>
                         <FormattedTextBox
-                            {...OmitKeys(this.props, ['children']).omit}
+                            {...this.props}
+                            setHeight={undefined}
                             setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
                             yPadding={10}
                             xPadding={10}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f5adc17d0..befb0200d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -259,6 +259,7 @@ export class MainView extends React.Component {
                 fa.faConciergeBell,
                 fa.faWindowRestore,
                 fa.faFolder,
+                fa.faFolderOpen,
                 fa.faMapPin,
                 fa.faMapMarker,
                 fa.faFingerprint,
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f3a5a5393..fbc7d7696 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -102,7 +102,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     @observable inOptions: boolean = false;
     @observable _controlButton: boolean = false;
-    @observable _lock: boolean = false;
 
     componentDidMount() {
         this.selectedDocListenerDisposer?.();
@@ -586,16 +585,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
                     </div>
                 </Tooltip>
-                <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>}>
-                    <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}>
-                        <FontAwesomeIcon icon="undo" color="white" size="lg" />
-                    </div>
-                </Tooltip>
             </div>
         );
     }
@@ -644,9 +633,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @action
     upDownButtons = (dirs: string, field: string) => {
         switch (field) {
-            case 'rot':
-                this.rotate(dirs === 'up' ? 0.1 : -0.1);
-                break;
             case 'Xps':
                 this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10));
                 break;
@@ -662,7 +648,6 @@ 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));
                 const doc = this.selectedDoc;
                 if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
                     const ink = Cast(doc.data, InkField)?.inkData;
@@ -684,7 +669,6 @@ 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));
                 const docu = this.selectedDoc;
                 if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
                     const ink = Cast(docu.data, InkField)?.inkData;
@@ -704,11 +688,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     };
 
     getField(key: string) {
-        //if (this.selectedDoc) {
         return Field.toString(this.selectedDoc?.[key] as Field);
-        // } else {
-        //     return undefined as Opt<string>;
-        // }
     }
 
     @computed get shapeXps() {
@@ -717,9 +697,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @computed get shapeYps() {
         return this.getField('y');
     }
-    @computed get shapeRot() {
-        return this.getField('rotation');
-    }
     @computed get shapeHgt() {
         return this.getField('_height');
     }
@@ -732,18 +709,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     set shapeYps(value) {
         this.selectedDoc && (this.selectedDoc.y = Number(value));
     }
-    set shapeRot(value) {
-        this.selectedDoc && (this.selectedDoc.rotation = Number(value));
-    }
     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);
     }
     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);
     }
 
     @computed get hgtInput() {
@@ -790,30 +760,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
             'Y:'
         );
     }
-    @computed get rotInput() {
-        return this.inputBoxDuo(
-            'rot',
-            this.shapeRot,
-            (val: string) => {
-                if (!isNaN(Number(val))) {
-                    this.rotate(Number(val) - Number(this.shapeRot));
-                    this.shapeRot = val;
-                }
-                return true;
-            },
-            '∠:',
-            'rot',
-            this.shapeRot,
-            (val: string) => {
-                if (!isNaN(Number(val))) {
-                    this.rotate(Number(val) - Number(this.shapeRot));
-                    this.shapeRot = val;
-                }
-                return true;
-            },
-            ''
-        );
-    }
 
     @observable private _fillBtn = false;
     @observable private _lineBtn = false;
@@ -1080,10 +1026,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @computed get transformEditor() {
         return (
             <div className="transform-editor">
-                {this.controlPointsButton}
+                {this.isInk ? this.controlPointsButton : null}
                 {this.hgtInput}
                 {this.XpsInput}
-                {this.rotInput}
             </div>
         );
     }
@@ -1194,17 +1139,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     </div>
                 )}
 
-                {this.isInk ? (
-                    <div className="propertiesView-transform">
-                        <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
-                            Transform
-                            <div className="propertiesView-transform-title-icon">
-                                <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
-                            </div>
+                <div className="propertiesView-transform">
+                    <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
+                        Transform
+                        <div className="propertiesView-transform-title-icon">
+                            <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
                         </div>
-                        {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
                     </div>
-                ) : null}
+                    {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
+                </div>
             </>
         );
     }
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 02dd15960..48c0150e6 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -5,7 +5,7 @@ import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { RichTextField } from '../../fields/RichTextField';
 import { DocCast, NumCast, StrCast } from '../../fields/Types';
-import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils';
+import { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZero } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { LinkManager } from '../util/LinkManager';
@@ -26,6 +26,7 @@ interface ExtraProps {
     // usePanelWidth: boolean;
     showSidebar: boolean;
     nativeWidth: number;
+    usePanelWidth?: boolean;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     ScreenToLocalTransform: () => Transform;
     sidebarAddDocument: (doc: Doc | Doc[], suffix: string) => boolean;
@@ -158,13 +159,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             .ScreenToLocalTransform()
             .translate(Doc.NativeWidth(this.props.dataDoc), 0)
             .scale(this.props.NativeDimScaling?.() || 1);
-    // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 :
-    //     this.props.usePanelWidth ? this.props.PanelWidth() :
-    //         (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
     panelWidth = () =>
         !this.props.showSidebar
             ? 0
-            : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP
+            : this.props.usePanelWidth // [DocumentType.RTF, DocumentType.MAP].includes(this.props.layoutDoc.type as any)
             ? this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1)
             : ((NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth()) / NumCast(this.props.nativeWidth);
     panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
@@ -224,20 +222,21 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 </div>
                 <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}>
                     <CollectionStackingView
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                        ref={this._stackRef}
+                        {...this.props}
+                        setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
+                        ref={this._stackRef}
                         PanelHeight={this.panelHeight}
                         PanelWidth={this.panelWidth}
                         docFilters={this.docFilters}
-                        scaleField={this.sidebarKey + '-scale'}
                         sortFunc={this.sortByLinkAnchorY}
                         setHeight={this.setHeightCallback}
                         isAnnotationOverlay={false}
                         select={emptyFunction}
                         NativeDimScaling={returnOne}
                         //childShowTitle={this.showTitle}
+                        isAnyChildContentActive={returnFalse}
                         childDocumentsActive={this.props.isContentActive}
                         whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                         childHideDecorationTitle={returnTrue}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 50cf8eba7..f7b9420a7 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -170,7 +170,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color));
             if (docColor) return docColor;
             const docView = props?.DocumentView?.();
-            const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, 'backgroundColor');
+            const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, StyleProp.BackgroundColor);
             if (!backColor) return undefined;
             return lightOrDark(backColor);
         case StyleProp.Hidden:
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 681ff66e0..45db240a9 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -79,7 +79,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     };
     componentDidMount() {
         !this._addedKeys && (this._addedKeys = new ObservableSet());
-        Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document)))
+        [...Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))), ...Array.from(Object.keys(this.props.docViews[0].props.Document))]
             .filter(key => key.startsWith('layout_'))
             .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
     }
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 57ff1b292..a266c9207 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
 import { Doc } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse, Utils } from '../../../Utils';
+import { returnFalse, returnZero, Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { StyleProp } from '../StyleProvider';
@@ -43,7 +43,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
             return (
                 <DocumentView
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'childLayoutTemplate', 'childLayoutString']).omit}
+                    {...this.props}
+                    NativeWidth={returnZero}
+                    NativeHeight={returnZero}
+                    suppressSetHeight={true}
                     onDoubleClick={this.onChildDoubleClick}
                     renderDepth={this.props.renderDepth + 1}
                     LayoutTemplate={this.props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 32f6207ed..c0220f804 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse } from '../../../Utils';
+import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -47,7 +47,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     @computed get content() {
         const index = NumCast(this.layoutDoc._itemIndex);
         const curDoc = this.childLayoutPairs?.[index];
-        const captionProps = { ...OmitKeys(this.props, ['setHeight']).omit, fieldKey: 'caption' };
+        const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
         const marginX = NumCast(this.layoutDoc['caption-xMargin']);
         const marginY = NumCast(this.layoutDoc['caption-yMargin']);
         const showCaptions = StrCast(this.layoutDoc._showCaption);
@@ -55,7 +55,9 @@ export class CollectionCarouselView extends CollectionSubView() {
             <>
                 <div className="collectionCarouselView-image" key="image">
                     <DocumentView
-                        {...OmitKeys(this.props, ['setHeight', 'NativeWidth', 'NativeHeight', 'childLayoutTemplate', 'childLayoutString']).omit}
+                        {...this.props}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
                         onDoubleClick={this.onContentDoubleClick}
                         onClick={this.onContentClick}
                         hideCaptions={showCaptions ? true : false}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 4941bc722..642d29d2a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnNone, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -23,7 +23,6 @@ import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { AudioWaveform } from '../AudioWaveform';
 import { CollectionSubView } from '../collections/CollectionSubView';
-import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { LabelBox } from '../nodes/LabelBox';
@@ -512,37 +511,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
 
     currentTimecode = () => this.currentTime;
 
-    @computed get renderDictation() {
-        const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
-        return !dictation ? null : (
-            <div
-                style={{
-                    position: 'absolute',
-                    height: '100%',
-                    top: this.timelineContentHeight,
-                    background: Colors.LIGHT_BLUE,
-                }}>
-                <DocumentView
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                    Document={dictation}
-                    PanelHeight={this.dictationHeight}
-                    isAnnotationOverlay={true}
-                    isDocumentActive={returnFalse}
-                    select={emptyFunction}
-                    NativeDimScaling={returnOne}
-                    xMargin={25}
-                    yMargin={10}
-                    ScreenToLocalTransform={this.dictationScreenToLocalTransform}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    removeDocument={returnFalse}
-                    moveDocument={returnFalse}
-                    addDocument={returnFalse}
-                    CollectionView={undefined}
-                    renderDepth={this.props.renderDepth + 1}></DocumentView>
-            </div>
-        );
-    }
-
     // renders selection region on timeline
     @computed get selectionContainer() {
         const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
@@ -638,7 +606,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                 PanelWidth={this.timelineContentWidth}
                             />
                         )}
-                        {/* {this.renderDictation} */}
 
                         <div
                             className="collectionStackedTimeline-hover"
@@ -817,16 +784,22 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
     // renders anchor LabelBox
     renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
         const anchor = observable({ view: undefined as any });
-        const focusFunc = (doc: Doc, options: DocFocusOptions) => this.props.playLink(mark);
+        const focusFunc = (doc: Doc, options: DocFocusOptions): number | undefined => {
+            this.props.playLink(mark);
+            return undefined;
+        };
         return {
             anchor,
             view: (
                 <DocumentView
                     key="view"
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                    {...this.props}
+                    NativeWidth={returnZero}
+                    NativeHeight={returnZero}
                     ref={action((r: DocumentView | null) => (anchor.view = r))}
                     Document={mark}
                     DataDoc={undefined}
+                    docViewPath={returnEmptyDoclist}
                     pointerEvents={this.noEvents ? returnNone : undefined}
                     styleProvider={this.props.styleProvider}
                     renderDepth={this.props.renderDepth + 1}
@@ -837,7 +810,16 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                     PanelHeight={height}
                     fitWidth={returnTrue}
                     ScreenToLocalTransform={screenXf}
+                    addDocTab={returnFalse}
+                    pinToPres={emptyFunction}
+                    whenChildContentsActiveChanged={emptyFunction}
                     focus={focusFunc}
+                    isContentActive={returnFalse}
+                    ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
+                    searchFilterDocs={returnEmptyDoclist}
+                    docFilters={returnEmptyFilter}
+                    docRangeFilters={returnEmptyFilter}
                     rootSelected={returnFalse}
                     onClick={script}
                     onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 553967b95..4a11e8f0b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -7,7 +7,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../Utils';
+import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -384,12 +384,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false;
 
     @observable _headerHeight = 0;
-    contentFunc = () => {
+    @computed get content() {
         const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
         const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
         const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
-        return [
-            <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
+        return (
+            <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
                 {!this.buttonMenu && !this.noviceExplainer ? null : (
                     <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}>
                         {this.buttonMenu}
@@ -428,9 +428,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                         </div>
                     </div>
                 </div>
-            </div>,
-        ];
-    };
+            </div>
+        );
+    }
     render() {
         TraceMobx();
 
@@ -439,7 +439,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
                 {!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeViewHasOverlay ? (
                     <CollectionFreeFormView
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                        {...this.props}
+                        setContentView={emptyFunction}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
+                        pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
                         isAnnotationOverlay={true}
                         isAnnotationOverlayScrollable={true}
                         childDocumentsActive={this.props.isDocumentActive}
@@ -451,10 +455,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                         moveDocument={this.moveAnnotationDocument}
                         bringToFront={emptyFunction}
                         renderDepth={this.props.renderDepth + 1}>
-                        {this.contentFunc}
+                        {this.content}
                     </CollectionFreeFormView>
                 ) : (
-                    this.contentFunc()
+                    this.content
                 )}
             </div>
         );
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 51624689e..bc25ad43a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -45,7 +45,7 @@ interface CollectionViewProps_ extends FieldViewProps {
 
     // property overrides for child documents
     childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
-    childDocumentsActive?: () => boolean; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
+    childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
     childFitWidth?: (child: Doc) => boolean;
     childShowTitle?: () => string;
     childOpacity?: () => number;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aed3683d4..d39668a5d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import { DateField } from '../../../../fields/DateField';
@@ -53,10 +53,14 @@ import { MarqueeView } from './MarqueeView';
 import React = require('react');
 
 export type collectionFreeformViewProps = {
+    noPointerWheel?: () => boolean; // turn off pointerwheel interactions (see PDFViewer)
+    NativeWidth?: () => number;
+    NativeHeight?: () => number;
+    originTopLeft?: boolean;
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
     viewDefDivClick?: ScriptField;
     childPointerEvents?: string;
-    scaleField?: string;
+    viewField?: string;
     noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
     engineProps?: any;
     getScrollHeight?: () => number | undefined;
@@ -97,8 +101,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private get isAnnotationOverlay() {
         return this.props.isAnnotationOverlay;
     }
-    private get scaleFieldKey() {
-        return this.props.scaleField || '_viewScale';
+    public get scaleFieldKey() {
+        return this.props.viewField ? this.props.viewField + '-viewScale' : '_viewScale';
+    }
+    private get panXFieldKey() {
+        return this.props.viewField ? this.props.viewField + '-panX' : '_panX';
+    }
+    private get panYFieldKey() {
+        return this.props.viewField ? this.props.viewField + '-panY' : '_panY';
     }
     private get borderWidth() {
         return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH;
@@ -149,21 +159,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                   );
     }
     @computed get nativeWidth() {
-        return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+        return this.props.NativeWidth?.() || (this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
     }
     @computed get nativeHeight() {
-        return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+        return this.props.NativeHeight?.() || (this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
     }
     @computed get cachedCenteringShiftX(): number {
         const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
-        return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+        return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedCenteringShiftY(): number {
         const dv = this.props.DocumentView?.();
         const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
         // if freeform has a native aspect, then the panel height needs to be adjusted to match it
         const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth();
-        return this.props.isAnnotationOverlay ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+        return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedGetLocalTransform(): Transform {
         return Transform.Identity()
@@ -236,13 +246,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     reverseNativeScaling = () => (this.fitContentsToBox ? true : false);
     // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
     // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
-    panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
-    panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
+    panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
+    panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
     zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
     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)`;
+        this.props.isAnnotationOverlay && 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();
@@ -293,7 +301,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     groupFocus = (anchor: Doc, options: DocFocusOptions) => {
-        options.docTransform = new Transform(-NumCast(this.rootDoc.panX) + NumCast(anchor.x), -NumCast(this.rootDoc.panY) + NumCast(anchor.y), 1);
+        options.docTransform = new Transform(-NumCast(this.rootDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.rootDoc[this.panYFieldKey]) + NumCast(anchor.y), 1);
         const res = this.props.focus(this.rootDoc, options);
         options.docTransform = undefined;
         return res;
@@ -301,7 +309,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     focus = (anchor: Doc, options: DocFocusOptions) => {
         const xfToCollection = options?.docTransform ?? Transform.Identity();
-        const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
+        const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
         const cantTransform = this.fitContentsToBox || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
         const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale || 0.75 : undefined);
 
@@ -772,7 +780,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         PresBox.Instance?.pauseAutoPres();
         const dx = e.deltaX;
         const dy = e.deltaY;
-        this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+        this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
     };
 
     @action
@@ -780,7 +788,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         PresBox.Instance?.pauseAutoPres();
         this.props.DocumentView?.().clearViewTransition();
         const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
-        this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+        this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
         this._lastX = e.clientX;
         this._lastY = e.clientY;
     };
@@ -979,13 +987,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) {
             const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20);
             this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
-            this.setPan(-localTransform.TranslateX / safeScale, NumCast(this.props.Document.scrollTop) * safeScale || -localTransform.TranslateY / safeScale);
+            this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.scrollTop) * safeScale) || -localTransform.TranslateY / safeScale);
         }
     };
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
-        if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+        if (this.props.noPointerWheel?.() || this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
         PresBox.Instance?.pauseAutoPres();
         if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
@@ -995,7 +1003,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // if ctrl is selected then zoom
                 if (e.ctrlKey) {
                     if (this.props.isContentActive(true)) {
-                        !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?
+                        this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
                     }
                 } // otherwise pan
                 else if (this.props.isContentActive(true)) {
@@ -1039,16 +1047,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) },
                     }),
                     {
-                        xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
-                        yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
+                        xrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
+                        yrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
                     }
                 );
 
-                const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()];
-                if (ranges.xrange.min >= panX + panelDim[0] / 2) panX = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds
-                else if (ranges.xrange.max <= panX - panelDim[0] / 2) panX = ranges.xrange.min - panelDim[0] / 2;
-                if (ranges.yrange.min >= panY + panelDim[1] / 2) panY = ranges.yrange.max + panelDim[1] / 2;
-                else if (ranges.yrange.max <= panY - panelDim[1] / 2) panY = ranges.yrange.min - panelDim[1] / 2;
+                const panelWidMax = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+                const panelWidMin = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+                const panelHgtMax = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+                const panelHgtMin = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+                if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this.props.originTopLeft ? 0 : panelWidMax / 2);
+                else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this.props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2);
+                if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this.props.originTopLeft ? 0 : panelHgtMax / 2);
+                else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2);
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
@@ -1078,8 +1089,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }, 10);
                 newPanY = minPanY;
             }
-            !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
-            !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
+            !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX);
+            !this.Document._horizontalScroll && (this.Document[this.panYFieldKey] = this.isAnnotationOverlay ? newPanY : panY);
         }
     }
 
@@ -1087,8 +1098,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     nudge = (x: number, y: number, nudgeTime: number = 500) => {
         if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) {
             this.setPan(
-                NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
-                NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
+                NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
+                NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
                 nudgeTime,
                 true
             );
@@ -1106,12 +1117,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         } else {
             const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
             docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
-            let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1;
-            if (zlast - docs.length > 100) {
-                for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
-                zlast = docs.length + 1;
+            let zlast = docs.length ? Math.max(docs.length, NumCast(docs.lastElement().zIndex)) : 1;
+            if (docs.lastElement() !== doc) {
+                if (zlast - docs.length > 100) {
+                    for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
+                    zlast = docs.length + 1;
+                }
+                doc.zIndex = zlast + 1;
             }
-            doc.zIndex = zlast + 1;
         }
     };
 
@@ -1134,8 +1147,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] };
         const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
-        this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
-        this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
+        this.layoutDoc[this.panXFieldKey] = NumCast(this.layoutDoc[this.panXFieldKey]) - newpan[0];
+        this.layoutDoc[this.panYFieldKey] = NumCast(this.layoutDoc[this.panYFieldKey]) - newpan[1];
     }
 
     calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
@@ -1158,8 +1171,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight();
         const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1);
         const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1);
-        const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
-        const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
+        const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
+        const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
         const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
         const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top));
         const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0;
@@ -1171,8 +1184,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             };
         }
         return {
-            panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
-            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
+            panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panXFieldKey]) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
+            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panYFieldKey]) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
         };
     };
 
@@ -1459,7 +1472,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) {
             // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
-            if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
+            if (this.props.viewField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
             else this.props.Document[this.scaleFieldKey] = Math.max(1, this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey]));
         }
 
@@ -1501,7 +1514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     cbounds => {
                         if (cbounds) {
                             const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2];
-                            const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)];
+                            const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])];
                             const pbounds = {
                                 x: cbounds.x - p[0] + c[0],
                                 y: cbounds.y - p[1] + c[1],
@@ -1511,8 +1524,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             if (Number.isFinite(pbounds.r - pbounds.x) && Number.isFinite(pbounds.b - pbounds.y)) {
                                 this.layoutDoc._width = pbounds.r - pbounds.x;
                                 this.layoutDoc._height = pbounds.b - pbounds.y;
-                                this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2;
-                                this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2;
+                                this.layoutDoc[this.panXFieldKey] = (cbounds.r + cbounds.x) / 2;
+                                this.layoutDoc[this.panYFieldKey] = (cbounds.b + cbounds.y) / 2;
                                 this.layoutDoc.x = pbounds.x;
                                 this.layoutDoc.y = pbounds.y;
                             }
@@ -1634,8 +1647,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0;
             const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0;
             if (deltaX !== 0 || deltaY !== 0) {
-                this.Document._panY = NumCast(this.Document._panY) + deltaY / 2;
-                this.Document._panX = NumCast(this.Document._panX) + deltaX / 2;
+                this.Document[this.panYFieldKey] = NumCast(this.Document[this.panYFieldKey]) + deltaY / 2;
+                this.Document[this.panXFieldKey] = NumCast(this.Document[this.panXFieldKey]) + deltaX / 2;
             }
         }
         e.stopPropagation();
@@ -1659,8 +1672,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20;
         const dim = Math.ceil(Math.sqrt(docs.length));
         docs.forEach((doc, i) => {
-            doc.x = NumCast(this.Document._panX) + (i % dim) * width - (width * dim) / 2;
-            doc.y = NumCast(this.Document._panY) + Math.floor(i / dim) * height - (height * dim) / 2;
+            doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2;
+            doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2;
         });
     };
 
@@ -1675,7 +1688,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         appearanceItems.push({
             description: 'Reset View',
             event: () => {
-                this.props.Document._panX = this.props.Document._panY = 0;
+                this.props.Document[this.panXFieldKey] = this.props.Document[this.panYFieldKey] = 0;
                 this.props.Document[this.scaleFieldKey] = 1;
             },
             icon: 'compress-arrows-alt',
@@ -1795,11 +1808,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
 
-    children = () => {
+    get children() {
         this.incrementalRender();
-        const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : [];
+        const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : this.props.children ? [this.props.children] : [];
         return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />];
-    };
+    }
 
     @computed get placeholder() {
         return (
@@ -1843,6 +1856,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 PanelHeight={this.props.PanelHeight}
                                 panX={this.panX}
                                 panY={this.panY}
+                                nativeDimScaling={this.nativeDim}
                                 zoomScaling={this.zoomScaling}
                                 layoutDoc={this.layoutDoc}
                                 isAnnotationOverlay={this.isAnnotationOverlay}
@@ -1877,6 +1891,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const wscale = nw ? this.props.PanelWidth() / nw : 1;
         return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale;
     }
+    nativeDim = () => this.nativeDimScaling;
 
     private groupDropDisposer?: DragManager.DragDropDisposer;
     protected createGroupEventsTarget = (ele: HTMLDivElement) => {
@@ -1912,7 +1927,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ref={r => {
                     this.createDashEventsTarget(r);
                     // prevent wheel events from passivly propagating up through containers
-                    r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
+                    !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
                 }}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
@@ -1985,7 +2000,8 @@ interface CollectionFreeFormViewPannableContentsProps {
     transform: () => string;
     zoomScaling: () => number;
     viewDefDivClick?: ScriptField;
-    children: () => JSX.Element[];
+    children?: React.ReactNode | undefined;
+    //children: () => JSX.Element[];
     transition?: string;
     presPaths: () => JSX.Element | null;
     presPinView?: boolean;
@@ -2079,7 +2095,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
                     width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable.  This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection
                     //willChange: "transform"
                 }}>
-                {this.props.children()}
+                {this.props.children}
                 {!this.props.brushView.width ? null : (
                     <div
                         className="collectionFreeFormView-brushView"
@@ -2107,6 +2123,7 @@ interface CollectionFreeFormViewBackgroundGridProps {
     PanelWidth: () => number;
     PanelHeight: () => number;
     isAnnotationOverlay?: boolean;
+    nativeDimScaling: () => number;
     zoomScaling: () => number;
     layoutDoc: Doc;
     cachedCenteringShiftX: number;
@@ -2124,10 +2141,10 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
         const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling();
         const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling();
         const renderGridSpace = gridSpace * this.props.zoomScaling();
-        const w = this.props.PanelWidth() + 2 * renderGridSpace;
-        const h = this.props.PanelHeight() + 2 * renderGridSpace;
+        const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace;
+        const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace;
         const strokeStyle = Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'rgba(255,255,255,0.5)' : 'rgba(0, 0,0,0.5)';
-        return (
+        return !this.props.nativeDimScaling() ? null : (
             <canvas
                 className="collectionFreeFormView-grid"
                 width={w}
@@ -2174,7 +2191,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
         const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
         let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
         while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
-        const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
+        const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
         ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
         Doc.linkFollowHighlight(dv?.props.Document, false);
     } else {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f16371592..043fe0bcf 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -237,6 +237,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        // if (this.props.pointerEvents?.() === 'none') return;
         this._downX = this._lastX = e.clientX;
         this._downY = this._lastY = e.clientY;
         if (!(e.nativeEvent as any).marqueeHit) {
@@ -345,6 +346,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     onClick = (e: React.MouseEvent): void => {
+        if (this.props.pointerEvents?.() === 'none') return;
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             if (Doc.ActiveTool === InkTool.None) {
                 if (!(e.nativeEvent as any).marqueeHit) {
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 9468c5f06..e8ae88ae5 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
@@ -186,7 +186,10 @@ export class CollectionGridView extends CollectionSubView() {
     getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
         return (
             <DocumentView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                {...this.props}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                setContentView={emptyFunction}
                 Document={layout}
                 DataDoc={layout.resolvedDataDoc as Doc}
                 isContentActive={this.isChildContentActive}
@@ -196,7 +199,7 @@ export class CollectionGridView extends CollectionSubView() {
                 whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                 onClick={this.onChildClickHandler}
                 renderDepth={this.props.renderDepth + 1}
-                dontCenter={this.props.Document.centerY ? '' : 'y'}
+                dontCenter={this.props.Document.centerY ? undefined : 'y'}
             />
         );
     }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index d47c9762c..fd9bcf681 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,23 +1,20 @@
 import React = require('react');
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, ObservableMap, trace, untracked } from 'mobx';
+import { action, computed, observable, ObservableMap, untracked } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, DocListCast, Field, StrListCast } from '../../../../fields/Doc';
+import { computedFn } from 'mobx-utils';
+import { Doc, Field, StrListCast } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { ImageField } from '../../../../fields/URLField';
 import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
-import { ContextMenuProps } from '../../ContextMenuItem';
 import { EditableView } from '../../EditableView';
 import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -215,9 +212,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    addNewKey = (key: string, defaultVal: any) => {
-        this.childDocs.forEach(doc => (doc[key] = defaultVal));
-    };
+    addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal));
 
     @undoBatch
     @action
@@ -303,9 +298,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    addRowRef = (doc: Doc, ref: HTMLDivElement) => {
-        this._rowEles.set(doc, ref);
-    };
+    addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref);
 
     @action
     setColRef = (index: number, ref: HTMLDivElement) => {
@@ -405,68 +398,12 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
-        const layoutItems: ContextMenuProps[] = [];
-        const docItems: ContextMenuProps[] = [];
-        const dataDoc = this.props.DataDoc || this.props.Document;
-
-        DocUtils.addDocumentCreatorMenuItems(
-            doc => {
-                FormattedTextBox.SelectOnLoad = StrCast(doc[Id]);
-                return this.addRow(doc);
-            },
-            this.addRow,
-            x,
-            y,
-            true
-        );
 
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string')
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document));
-                        if (created) {
-                            if (this.props.Document.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, this.props.Document);
-                            }
-                            return this.addRow(created);
-                        }
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length)
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
-                        if (created) {
-                            const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document;
-                            if (container.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, container);
-                                return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
-                            }
-                            return this.addRow(created) || false;
-                        }
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' });
-        !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
+        DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true);
+
         ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
             Doc.GetProto(this.props.Document)[name] = '';
-            const created = Docs.Create.TextDocument('', { title: name, _autoHeight: true });
-            if (created) {
-                if (this.props.Document.isTemplateDoc) {
-                    Doc.MakeMetadataFieldTemplate(created, this.props.Document);
-                }
-                this.addRow(created);
-            }
+            this.addRow(Docs.Create.TextDocument('', { title: name, _autoHeight: true }));
         });
         ContextMenu.Instance.displayMenu(x, y, undefined, true);
     };
@@ -543,9 +480,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    closeColumnMenu = () => {
-        this._columnMenuIndex = undefined;
-    };
+    closeColumnMenu = () => (this._columnMenuIndex = undefined);
 
     @action
     openFilterMenu = (index: number) => {
@@ -596,19 +531,14 @@ export class CollectionSchemaView extends CollectionSubView() {
     getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
 
     removeFieldFilters = (field: string) => {
-        this.getFieldFilters(field).forEach(filter => {
-            Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove');
-        });
+        this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'));
     };
 
     onFilterKeyDown = (e: React.KeyboardEvent) => {
+        //prettier-ignore
         switch (e.key) {
-            case 'Enter':
-                this.closeFilterMenu(true);
-                break;
-            case 'Escape':
-                this.closeFilterMenu(false);
-                break;
+            case 'Enter' : this.closeFilterMenu(true); break;
+            case 'Escape': this.closeFilterMenu(false);break;
         }
     };
 
@@ -903,6 +833,7 @@ interface CollectionSchemaViewDocsProps {
 class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
     tableWidthFunc = () => this.props.schema.tableWidth;
     rowHeightFunc = () => CollectionSchemaView._rowHeight;
+    childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -CollectionSchemaView._rowHeight - index * this.rowHeightFunc()));
     render() {
         return (
             <div className="schema-table-content">
@@ -932,7 +863,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
                                 docRangeFilters={this.props.schema.childDocRangeFilters}
                                 searchFilterDocs={this.props.schema.searchFilterDocs}
                                 rootSelected={this.props.schema.rootSelected}
-                                ScreenToLocalTransform={Transform.Identity}
+                                ScreenToLocalTransform={this.childScreenToLocal(index)}
                                 bringToFront={emptyFunction}
                                 isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
                                 isContentActive={emptyFunction}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index cbdaeb933..d88d67c94 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -39,18 +39,12 @@ export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps>
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction);
+        setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false);
     };
 
     render() {
         return (
-            <div
-                className="schema-column-header"
-                style={{ width: this.props.columnWidths[this.props.columnIndex] }}
-                onPointerDown={this.onPointerDown}
-                ref={(col: HTMLDivElement | null) => {
-                    col && this.props.setColRef(this.props.columnIndex, col);
-                }}>
+            <div className="schema-column-header" style={{ width: this.props.columnWidths[this.props.columnIndex] }} onPointerDown={this.onPointerDown} ref={col => col && this.props.setColRef(this.props.columnIndex, col)}>
                 <div className="schema-column-resizer left" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex)}></div>
                 <div className="schema-column-title">{this.fieldKey}</div>
 
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 384975b45..a6acf882c 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, DateCast, NumCast } from '../../../fields/Types';
 import { AudioField, nullAudio } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
@@ -19,7 +19,6 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
 import './AudioBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
-import { SelectionManager } from '../../util/SelectionManager';
 import { PinProps, PresBox } from './trails';
 
 /**
@@ -655,7 +654,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return (
             <CollectionStackedTimeline
                 ref={action((r: any) => (this._stackedTimeline = r))}
-                {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit}
+                {...this.props}
+                CollectionFreeFormDocumentView={undefined}
                 fieldKey={this.annotationKey}
                 dictationKey={this.fieldKey + '-dictation'}
                 mediaPath={this.path}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ecffe6c4f..ace388c57 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -3,7 +3,7 @@ import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -122,7 +122,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                         ref={r => {
                             //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
                         }}
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                        {...this.props}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
                         isContentActive={returnFalse}
                         isDocumentActive={returnFalse}
                         styleProvider={this.docStyleProvider}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 091a6b050..c7bf37a45 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -12,7 +12,7 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -512,7 +512,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 this.props.isDocumentActive?.() &&
                 !this.props.onBrowseClick?.() &&
                 !this.Document.ignoreClick &&
-                !e.ctrlKey &&
                 e.button === 0 &&
                 this.pointerEvents !== 'none' &&
                 !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
@@ -705,22 +704,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const appearance = cm.findByDescription('UI Controls...');
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
-            !Doc.noviceMode &&
-                appearanceItems.push({
-                    description: 'Add a Field',
-                    event: () => {
-                        const alias = Doc.MakeAlias(this.rootDoc);
-                        alias.layout = FormattedTextBox.LayoutString('newfield');
-                        alias.title = 'newfield';
-                        alias._height = 35;
-                        alias._width = 100;
-                        alias.syncLayoutFieldWithTitle = true;
-                        alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
-                        alias.y = NumCast(this.rootDoc.y);
-                        this.props.addDocument?.(alias);
-                    },
-                    icon: 'eye',
-                });
             LinkManager.Links(this.Document).length &&
                 appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
             !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
@@ -863,7 +846,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
     setHeight = (height: number) => (this.layoutDoc._height = height);
     setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
-    isContentActive = (outsideReaction?: boolean) => {
+    isContentActive = (outsideReaction?: boolean): boolean | undefined => {
         return this.props.isContentActive() === false
             ? false
             : Doc.ActiveTool !== InkTool.None ||
@@ -1064,7 +1047,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     };
 
-    captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
+    captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
     @computed get innards() {
         TraceMobx();
         const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1;
@@ -1079,7 +1062,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     maxHeight: `max(100%, ${20 * ffscale()}px)`,
                 }}>
                 <FormattedTextBox
-                    {...OmitKeys(this.props, ['children']).omit}
+                    {...this.props}
                     yPadding={10}
                     xPadding={10}
                     fieldKey={this.showCaption}
@@ -1090,7 +1073,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     dontScale={true}
                     renderDepth={this.props.renderDepth}
                     isContentActive={this.isContentActive}
-                    onClick={this.onClickFunc}
                 />
             </div>
         );
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e53422ab7..8d3534a5c 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -20,7 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
 
     select: (isCtrlPressed: boolean) => void;
     isContentActive: (outsideReaction?: boolean) => boolean | undefined;
-    isDocumentActive?: () => boolean;
+    isDocumentActive?: () => boolean | undefined;
     isSelected: (outsideReaction?: boolean) => boolean;
     setHeight?: (height: number) => void;
     NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 48e54b722..b5193cd20 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -409,8 +409,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     }
 
-    contentFunc = () => [this.content];
-
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     @observable _marqueeing: number[] | undefined;
@@ -473,7 +471,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 }}>
                 <CollectionFreeFormView
                     ref={this._ffref}
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                    {...this.props}
+                    setContentView={emptyFunction}
+                    NativeWidth={returnZero}
+                    NativeHeight={returnZero}
                     renderDepth={this.props.renderDepth + 1}
                     fieldKey={this.annotationKey}
                     styleProvider={this.props.styleProvider}
@@ -487,11 +488,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     focus={this.focus}
                     getScrollHeight={this.getScrollHeight}
                     NativeDimScaling={returnOne}
+                    isAnyChildContentActive={returnFalse}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     removeDocument={this.removeDocument}
                     moveDocument={this.moveDocument}
                     addDocument={this.addDocument}>
-                    {this.contentFunc}
+                    {this.content}
                 </CollectionFreeFormView>
                 {this.annotationLayer}
                 {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index ea8f47b39..36be7d257 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -8,7 +8,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
@@ -635,7 +635,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 .map(marker => (
                                     <MapBoxInfoWindow
                                         key={marker[Id]}
-                                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                                        {...this.props}
+                                        setContentView={emptyFunction}
                                         place={marker}
                                         markerMap={this.markerMap}
                                         PanelWidth={this.infoWidth}
@@ -674,6 +675,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         rootDoc={this.rootDoc}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
+                        usePanelWidth={true}
                         showSidebar={this.SidebarShown}
                         nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 00bedafbe..d65ef9c4c 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
@@ -52,7 +52,8 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
                     <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
                         <CollectionStackingView
                             ref={r => (this._stack = r)}
-                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                            {...this.props}
+                            setContentView={emptyFunction}
                             Document={this.props.place}
                             DataDoc={undefined}
                             fieldKey="data"
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5f207cc0d..a254edd87 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,16 +5,20 @@ import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { InkTool } from '../../../fields/InkField';
+import { ComputedField } from '../../../fields/ScriptField';
+import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { KeyCodes } from '../../util/KeyCodes';
+import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
@@ -41,7 +45,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     private _initialScrollTarget: Opt<Doc>;
     private _pdfViewer: PDFViewer | undefined;
     private _searchRef = React.createRef<HTMLInputElement>();
-    private _selectReactionDisposer: IReactionDisposer | undefined;
+    private _disposers: { [name: string]: IReactionDisposer } = {};
     private _sidebarRef = React.createRef<SidebarAnnos>();
 
     @observable private _searching: boolean = false;
@@ -184,11 +188,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     componentWillUnmount() {
-        this._selectReactionDisposer?.();
+        Object.values(this._disposers).forEach(disposer => disposer?.());
     }
     componentDidMount() {
         this.props.setContentView?.(this);
-        this._selectReactionDisposer = reaction(
+        this._disposers.select = reaction(
             () => this.props.isSelected(),
             () => {
                 document.removeEventListener('keydown', this.onKeyDown);
@@ -196,6 +200,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             },
             { fireImmediately: true }
         );
+        this._disposers.scroll = reaction(
+            () => this.rootDoc.scrollTop,
+            () => {
+                if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '-panY'])) instanceof ComputedField)) {
+                    this.props.Document[this.SidebarKey + '-panY'] = ComputedField.MakeFunction('this.scrollTop');
+                }
+                this.props.Document[this.SidebarKey + '-viewScale'] = 1;
+                this.props.Document[this.SidebarKey + '-panX'] = 0;
+            }
+        );
     }
 
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
@@ -303,7 +317,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     // adding external documents; to sidebar key
     // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
-    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
         return this.addDocument(doc, sidebarKey);
     };
@@ -428,6 +442,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
+        funcs.push({
+            description: 'Toggle Sidebar Type',
+            event: () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform),
+            icon: 'expand-arrows-alt',
+        });
         funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
         funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
         //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
@@ -467,12 +486,87 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         );
     }
 
+    public get SidebarKey() {
+        return this.fieldKey + '-sidebar';
+    }
+    @computed get pdfScale() {
+        const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+        const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth);
+        const pdfRatio = pdfNativeWidth / nativeWidth;
+        return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth;
+    }
+    @computed get sidebarNativeWidth() {
+        return this.sidebarWidth() / this.pdfScale;
+    }
+    @computed get sidebarNativeHeight() {
+        return this.props.PanelHeight() / this.pdfScale;
+    }
+    sidebarNativeWidthFunc = () => this.sidebarNativeWidth;
+    sidebarNativeHeightFunc = () => this.sidebarNativeHeight;
+    sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
+    sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
+    sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0);
+    @computed get sidebarCollection() {
+        const renderComponent = (tag: string) => {
+            const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView;
+            return ComponentTag === CollectionStackingView ? (
+                <SidebarAnnos
+                    ref={this._sidebarRef}
+                    {...this.props}
+                    rootDoc={this.rootDoc}
+                    layoutDoc={this.layoutDoc}
+                    dataDoc={this.dataDoc}
+                    setHeight={emptyFunction}
+                    nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+                    showSidebar={this.SidebarShown}
+                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                    sidebarAddDocument={this.sidebarAddDocument}
+                    moveDocument={this.moveDocument}
+                    removeDocument={this.removeDocument}
+                    //PanelWidth={this.sidebarWidth}
+                    //ScreenToLocalTransform={this.sidebarScreenToLocal}
+                />
+            ) : (
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
+                    <ComponentTag
+                        {...this.props}
+                        setContentView={emptyFunction} // override setContentView to do nothing
+                        NativeWidth={this.sidebarNativeWidthFunc}
+                        NativeHeight={this.sidebarNativeHeightFunc}
+                        PanelHeight={this.props.PanelHeight}
+                        PanelWidth={this.sidebarWidth}
+                        xPadding={0}
+                        yPadding={0}
+                        viewField={this.SidebarKey}
+                        isAnnotationOverlay={false}
+                        originTopLeft={true}
+                        isAnyChildContentActive={this.isAnyChildContentActive}
+                        select={emptyFunction}
+                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                        removeDocument={this.sidebarRemDocument}
+                        moveDocument={this.sidebarMoveDocument}
+                        addDocument={this.sidebarAddDocument}
+                        CollectionView={undefined}
+                        ScreenToLocalTransform={this.sidebarScreenToLocal}
+                        renderDepth={this.props.renderDepth + 1}
+                        noSidebar={true}
+                        fieldKey={this.SidebarKey}
+                    />
+                </div>
+            );
+        };
+        return (
+            <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
+                {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+            </div>
+        );
+    }
     isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
     @computed get renderPdfView() {
         TraceMobx();
         const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
-        return (
+        return !this._pdf ? null : (
             <div
                 className={'pdfBox'}
                 onContextMenu={this.specificContextMenu}
@@ -496,7 +590,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         addDocTab={this.sidebarAddDocTab}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
-                        pdf={this._pdf!}
+                        pdf={this._pdf}
                         focus={this.focus}
                         url={this.pdfUrl!.url.pathname}
                         isContentActive={this.isPdfContentActive}
@@ -510,22 +604,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         crop={this.crop}
                     />
                 </div>
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
-                    <SidebarAnnos
-                        ref={this._sidebarRef}
-                        {...this.props}
-                        rootDoc={this.rootDoc}
-                        layoutDoc={this.layoutDoc}
-                        dataDoc={this.dataDoc}
-                        setHeight={emptyFunction}
-                        nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
-                        showSidebar={this.SidebarShown}
-                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                        sidebarAddDocument={this.sidebarAddDocument}
-                        moveDocument={this.moveDocument}
-                        removeDocument={this.removeDocument}
-                    />
-                </div>
+                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
                 {this.settingsPanel()}
             </div>
         );
@@ -535,21 +614,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
     render() {
         TraceMobx();
-        if (this._pdf) {
-            if (!this.props.thumbShown?.()) {
-                return this.renderPdfView;
-            }
-            return null;
-        }
+        if (this.props.thumbShown?.()) return null;
+        const pdfView = this.renderPdfView;
 
         const href = this.pdfUrl?.url.href;
-        if (href) {
+        if (!pdfView && href) {
             if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
             else {
                 if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
                 PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
             }
         }
-        return this.renderTitleBox;
+        return pdfView ?? this.renderTitleBox;
     }
 }
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 61e4894f0..db11a7776 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { AudioField, VideoField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnFalse, returnOne } from '../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
@@ -277,7 +277,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
         dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
         this.dataDoc[this.fieldKey + '-dictation'] = dictationText;
     };
-    contentFunc = () => [this.threed, this.content];
     videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth();
     formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
     render() {
@@ -287,7 +286,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                 <div className="videoBox-viewer">
                     <div style={{ position: 'relative', height: this.videoPanelHeight() }}>
                         <CollectionFreeFormView
-                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                            {...this.props}
+                            setContentView={emptyFunction}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
                             PanelHeight={this.videoPanelHeight}
                             PanelWidth={this.props.PanelWidth}
                             focus={this.props.focus}
@@ -296,6 +298,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                             select={emptyFunction}
                             isContentActive={returnFalse}
                             NativeDimScaling={returnOne}
+                            isAnyChildContentActive={returnFalse}
                             whenChildContentsActiveChanged={emptyFunction}
                             removeDocument={returnFalse}
                             moveDocument={returnFalse}
@@ -304,17 +307,19 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                             ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                             renderDepth={this.props.renderDepth + 1}
                             ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
-                            {this.contentFunc}
+                            <>
+                                {this.threed}
+                                {this.content}
+                            </>
                         </CollectionFreeFormView>
                     </div>
                     <div style={{ position: 'relative', height: this.formattedPanelHeight() }}>
                         {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
                             <FormattedTextBox
-                                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                                {...this.props}
                                 Document={this.dataDoc[this.fieldKey + '-dictation']}
                                 fieldKey={'text'}
                                 PanelHeight={this.formattedPanelHeight}
-                                isAnnotationOverlay={true}
                                 select={emptyFunction}
                                 isContentActive={emptyFunction}
                                 NativeDimScaling={returnOne}
@@ -324,7 +329,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                                 removeDocument={returnFalse}
                                 moveDocument={returnFalse}
                                 addDocument={returnFalse}
-                                CollectionView={undefined}
                                 renderDepth={this.props.renderDepth + 1}
                                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox>
                         )}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0b88f5fe3..0570c7fcb 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -9,7 +9,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
@@ -894,8 +894,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     playing = () => this._playing;
 
-    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
     scaling = () => this.props.NativeDimScaling?.() || 1;
 
     panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
@@ -1068,7 +1066,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             left: (this.props.PanelWidth() - this.panelWidth()) / 2,
                         }}>
                         <CollectionFreeFormView
-                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                            {...this.props}
+                            setContentView={emptyFunction}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
                             renderDepth={this.props.renderDepth + 1}
                             fieldKey={this.annotationKey}
                             CollectionView={undefined}
@@ -1076,6 +1077,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             annotationLayerHostsContent={true}
                             PanelWidth={this.panelWidth}
                             PanelHeight={this.panelHeight}
+                            isAnyChildContentActive={returnFalse}
                             ScreenToLocalTransform={this.screenToLocalTransform}
                             docFilters={this.timelineDocFilter}
                             select={emptyFunction}
@@ -1084,7 +1086,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             removeDocument={this.removeDocument}
                             moveDocument={this.moveDocument}
                             addDocument={this.addDocWithTimecode}>
-                            {this.contentFunc}
+                            {this.youtubeVideoId ? this.youtubeContent : this.content}
                         </CollectionFreeFormView>
                     </div>
                     {this.annotationLayer}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d57518a8d..66d0fd385 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
@@ -305,7 +305,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
             if (scrollTo !== undefined) {
                 if (this._initialScroll === undefined) {
-                    this.goTo(scrollTo, options.zoomTime ?? 500, options.easeFunc);
+                    const focusTime = options.zoomTime ?? 500;
+                    this.goTo(scrollTo, focusTime, options.easeFunc);
+                    return focusTime;
                 } else {
                     this._initialScroll = scrollTo;
                 }
@@ -904,9 +906,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
         const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
-        const renderAnnotations = (docFilters?: () => string[]) => (
+        const renderAnnotations = (docFilters: () => string[]) => (
             <CollectionFreeFormView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                {...this.props}
+                setContentView={emptyFunction}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
                 renderDepth={this.props.renderDepth + 1}
                 isAnnotationOverlay={true}
                 fieldKey={this.annotationKey}
@@ -921,6 +926,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 dropAction={'alias'}
                 docFilters={docFilters}
                 select={emptyFunction}
+                isAnyChildContentActive={returnFalse}
                 bringToFront={emptyFunction}
                 styleProvider={this.childStyleProvider}
                 whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ee4249b02..d8afc3227 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -20,7 +20,7 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils';
 import { ComputedField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
@@ -1739,12 +1739,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             }
         }
     };
-    fitContentsToBox = () => this.props.Document._fitContentsToBox;
+    fitContentsToBox = () => BoolCast(this.props.Document._fitContentsToBox);
     sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
     sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
         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);
@@ -1785,8 +1783,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 className="formattedTextBox-sidebar-handle"
                 onPointerDown={this.sidebarDown}
                 style={{
-                    backgroundColor: backgroundColor,
-                    color: color,
+                    backgroundColor,
+                    color,
                     opacity: annotated ? 1 : undefined,
                 }}>
                 <FontAwesomeIcon icon={'comment-alt'} />
@@ -1800,33 +1798,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 <SidebarAnnos
                     ref={this._sidebarRef}
                     {...this.props}
-                    fieldKey={this.fieldKey}
                     rootDoc={this.rootDoc}
                     layoutDoc={this.layoutDoc}
                     dataDoc={this.dataDoc}
-                    ScreenToLocalTransform={this.sidebarScreenToLocal}
+                    usePanelWidth={true}
                     nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
-                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     showSidebar={this.SidebarShown}
-                    PanelWidth={this.sidebarWidth}
-                    setHeight={this.setSidebarHeight}
+                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     sidebarAddDocument={this.sidebarAddDocument}
                     moveDocument={this.moveDocument}
                     removeDocument={this.removeDocument}
+                    ScreenToLocalTransform={this.sidebarScreenToLocal}
+                    fieldKey={this.fieldKey}
+                    PanelWidth={this.sidebarWidth}
+                    setHeight={this.setSidebarHeight}
                 />
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
                     <ComponentTag
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                        {...this.props}
+                        setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
                         PanelHeight={this.props.PanelHeight}
                         PanelWidth={this.sidebarWidth}
                         xPadding={0}
                         yPadding={0}
-                        scaleField={this.SidebarKey + '-scale'}
+                        viewField={this.SidebarKey}
                         isAnnotationOverlay={false}
                         select={emptyFunction}
+                        isAnyChildContentActive={returnFalse}
                         NativeDimScaling={this.sidebarContentScaling}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         removeDocument={this.sidebarRemDocument}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c5060a2c2..6ab541207 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -7,7 +7,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, returnNone, smoothScroll, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
@@ -16,7 +16,7 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { DocFocusOptions, DocumentViewProps, OpenWhere } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { StyleProp } from '../StyleProvider';
@@ -512,7 +512,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this.props.styleProvider?.(doc, props, property);
     };
 
-    renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
+    renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => (
         <div
             className="pdfViewerDash-overlay"
             style={{
@@ -522,9 +522,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
             }}>
             <CollectionFreeFormView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                {...this.props}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                setContentView={emptyFunction} // override setContentView to do nothing
+                pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
                 renderDepth={this.props.renderDepth + 1}
-                pointerEvents={returnNone}
                 isAnnotationOverlay={true}
                 fieldKey={this.props.fieldKey + '-annotations'}
                 CollectionView={undefined}
@@ -534,6 +537,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 PanelHeight={this.panelHeight}
                 PanelWidth={this.panelWidth}
                 ScreenToLocalTransform={this.overlayTransform}
+                isAnyChildContentActive={returnFalse}
+                isAnnotationOverlayScrollable={true}
                 dropAction={'alias'}
                 docFilters={docFilters}
                 select={emptyFunction}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 553c4525c..cc024d83a 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1213,7 +1213,7 @@ export namespace Doc {
         return doc[StrCast(doc.layoutKey, 'layout')];
     }
     export function LayoutFieldKey(doc: Doc): string {
-        return StrCast(Doc.Layout(doc).layout).split("'")[1];
+        return StrCast(Doc.Layout(doc)[StrCast(doc.layoutKey, 'layout')]).split("'")[1]; // bcz: TODO check on this .  used to always reference 'layout', now it uses the layout speicfied by the current layoutKey
     }
     export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
         return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
@@ -1222,9 +1222,10 @@ export namespace Doc {
         return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeWidth'], useWidth ? doc[WidthSym]() : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
-        const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0) : 0;
-        const nheight = doc ? (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]() : 0;
-        return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
+        if (!doc) return 0;
+        const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]();
+        const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0);
+        return NumCast(doc._nativeHeight, nheight || dheight);
     }
     export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
         doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width;
-- 
cgit v1.2.3-70-g09d2

