From 52a435b09013619209b8bcc6758baeca47d5d350 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 8 Dec 2022 10:08:33 -0500
Subject: cleaned up animation effects to not reference presBox.  fixed anchors
 in text to have link properties set properly from properties view.

---
 src/client/views/nodes/MapBox/MapBox.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 6479e933e..b0f6f8358 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -7,7 +7,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, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
@@ -638,6 +638,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
                             annotationLayer={this._annotationLayer.current}
+                            selectionText={returnEmptyString}
                             mainCont={this._mainCont.current}
                         />
                     )}
-- 
cgit v1.2.3-70-g09d2


From 67316c700980fe653c48840407dc9d66a7ed8a2b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 6 Jan 2023 00:35:43 -0500
Subject: added zoom box highlighting of text anchors in pdf/web pages for
 link/pres following. Added serial/parallel option for presentation group with
 up.  Added direct pinning of text seletions to trails.  fixed 'hide' option
 for preselements to work with hidebefore/after

---
 src/client/util/DocumentManager.ts                 | 67 ++++++++++++-----
 src/client/util/LinkFollower.ts                    |  9 ++-
 src/client/views/MainView.tsx                      |  1 +
 src/client/views/MarqueeAnnotator.tsx              | 42 ++++++-----
 src/client/views/collections/CollectionSubView.tsx |  2 -
 src/client/views/collections/TabDocView.tsx        |  5 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++-
 src/client/views/nodes/DocumentContentsView.tsx    |  2 +-
 src/client/views/nodes/DocumentView.scss           | 16 +++++
 src/client/views/nodes/DocumentView.tsx            | 38 +++++++++-
 src/client/views/nodes/MapBox/MapBox.tsx           |  9 ---
 src/client/views/nodes/PDFBox.tsx                  |  7 ++
 src/client/views/nodes/WebBox.scss                 |  1 +
 src/client/views/nodes/WebBox.tsx                  | 27 +++++--
 src/client/views/nodes/trails/PresBox.tsx          | 83 ++++++++++++++--------
 src/client/views/nodes/trails/PresElementBox.tsx   |  3 +-
 src/client/views/pdf/PDFViewer.tsx                 | 10 ++-
 src/fields/Doc.ts                                  | 16 +++--
 18 files changed, 241 insertions(+), 106 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d2e9e17b4..70fe7f2c0 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,8 +1,8 @@
-import { action, observable, runInAction } from 'mobx';
-import { Doc, Opt } from '../../fields/Doc';
+import { action, observable, ObservableSet, runInAction } from 'mobx';
+import { AnimationSym, Doc, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { listSpec } from '../../fields/Schema';
-import { Cast, DocCast } from '../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -42,7 +42,7 @@ export class DocumentManager {
     callAddViewFuncs = (view: DocumentView) => {
         const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
         if (callFuncs.length) {
-            this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => callFuncs.includes(vc));
+            this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc));
             const intTimer = setInterval(
                 () => {
                     if (!view.ComponentView?.incrementalRendering?.()) {
@@ -147,14 +147,15 @@ export class DocumentManager {
 
     public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
         const found =
-            Array.from(DocumentManager.Instance.DocumentViews).find(
-                dv =>
-                    ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
-                    ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
-            )?.rootDoc ?? toFind;
+            // Array.from(DocumentManager.Instance.DocumentViews).find(
+            //     dv =>
+            //         ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
+            //         ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
+            // )?.rootDoc ??
+            toFind;
         return this.getDocumentViewById(found[Id], preferredCollection);
     }
-    
+
     public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
         const views: DocumentView[] = [];
         Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view));
@@ -167,11 +168,12 @@ export class DocumentManager {
     };
     public getDocumentViews(toFindIn: Doc): DocumentView[] {
         const toFind =
-            Array.from(DocumentManager.Instance.DocumentViews).find(
-                dv =>
-                    ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
-                    ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
-            )?.rootDoc ?? toFindIn;
+            // Array.from(DocumentManager.Instance.DocumentViews).find(
+            //     dv =>
+            //         ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
+            //         ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
+            // )?.rootDoc ??
+            toFindIn;
 
         const toReturn: DocumentView[] = [];
         const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath));
@@ -203,6 +205,11 @@ export class DocumentManager {
         }
     }
 
+    public static removeOverlayViews() {
+        DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined)));
+        DocumentManager._overlayViews?.clear();
+    }
+    static _overlayViews = new ObservableSet<DocumentView>();
     static addView = (doc: Doc, finished?: () => void) => {
         CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
         finished?.();
@@ -225,7 +232,7 @@ export class DocumentManager {
                 docView?.props.bringToFront(resolvedTarget);
             });
         }
-        const focusAndFinish = (didFocus: boolean) => {
+        const focusAndFinish = action((didFocus: boolean) => {
             const finalTargetDoc = resolvedTarget;
             if (options.toggleTarget) {
                 if (!didFocus && !wasHidden) {
@@ -236,12 +243,26 @@ export class DocumentManager {
                 finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
                 !options.noSelect && docView?.select(false);
             }
+            if (targetDoc.textHtml && options.zoomTextSelections) {
+                const containerView = DocumentManager.Instance.getFirstDocumentView(finalTargetDoc);
+                if (containerView) {
+                    containerView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect));
+                    containerView.textHtmlOverlay = StrCast(targetDoc.textHtml);
+                    DocumentManager._overlayViews.add(containerView);
+                    if (Doc.UnhighlightTimer) {
+                        Doc.AddUnHighlightWatcher(() => {
+                            DocumentManager.removeOverlayViews();
+                            containerView.htmlOverlayEffect = '';
+                        });
+                    } else setTimeout(() => (containerView.htmlOverlayEffect = ''));
+                }
+            }
             finished?.();
-        };
+        });
         const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
         if (annoContainerView) {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
-                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
+                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
             }
             if (!docView && targetDoc.type !== DocumentType.MARKER) {
                 annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
@@ -254,7 +275,15 @@ export class DocumentManager {
         const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
         const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (focusView) {
-            !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
+            if (focusView.rootDoc === originalTarget) {
+                if (!options.noSelect) Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
+                else {
+                    focusView.rootDoc[AnimationSym] = options.effect;
+                    if (Doc.UnhighlightTimer) {
+                        Doc.AddUnHighlightWatcher(action(() => (focusView.rootDoc[AnimationSym] = undefined)));
+                    }
+                }
+            }
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
                 focusView.focus(originalTarget, {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 94badbb44..d5ef9fab6 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,12 +1,10 @@
 import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, Opt, WidthSym } from '../../fields/Doc';
-import { listSpec } from '../../fields/Schema';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
-import { PresEffect, PresEffectDirection } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { UndoManager } from './UndoManager';
@@ -59,7 +57,7 @@ export class LinkFollower {
                 createTabForTarget(false);
             } else {
                 // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
-                docViewProps.focus(sourceDoc, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget });
+                docViewProps.focus(sourceDoc, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomTime: 1000, zoomScale: 1, afterFocus: createTabForTarget });
             }
         };
         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
@@ -70,7 +68,7 @@ export class LinkFollower {
             docViewProps.ContainingCollectionDoc,
             action(() => {
                 batch.end();
-                Doc.AddUnlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
+                Doc.AddUnHighlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
             }),
             altKey ? true : undefined
         );
@@ -110,6 +108,7 @@ export class LinkFollower {
                         easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
                         effect: sourceDoc,
                         originatingDoc: sourceDoc,
+                        zoomTextSelections: false,
                     };
                     if (target.TourMap) {
                         const fieldKey = Doc.LayoutFieldKey(target);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 42e259eed..39cc9ba8e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -482,6 +482,7 @@ export class MainView extends React.Component {
     }
 
     globalPointerDown = action((e: PointerEvent) => {
+        DocumentManager.removeOverlayViews();
         Doc.linkFollowUnhighlight();
         AudioBox.Enabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 2fdb59361..bf1242346 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -18,7 +18,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
 
 export interface MarqueeAnnotatorProps {
     rootDoc: Doc;
-    down: number[];
+    down?: number[];
     iframe?: () => undefined | HTMLIFrameElement;
     scrollTop: number;
     scaling?: () => number;
@@ -45,6 +45,17 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
     @observable private _width: number = 0;
     @observable private _height: number = 0;
 
+    constructor(props: any) {
+        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.OnAudio = unimplementedFunction;
+        AnchorMenu.Instance.Highlight = this.highlight;
+        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
+        AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
+    }
+
     @action
     static clearAnnotations(savedAnnotations: ObservableMap<number, HTMLDivElement[]>) {
         AnchorMenu.Instance.Status = 'marquee';
@@ -54,24 +65,21 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         savedAnnotations.clear();
     }
 
-    @action componentDidMount() {
-        // set marquee x and y positions to the spatially transformed position
-        const boundingRect = this.props.mainCont.getBoundingClientRect();
-        this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
-        this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
-        this._height = this._width = 0;
+    @action gotDownPoint() {
+        if (!this._width && !this._height) {
+            const downPt = this.props.down!;
+            // set marquee x and y positions to the spatially transformed position
+            const boundingRect = this.props.mainCont.getBoundingClientRect();
+            this._startX = this._left = (downPt[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
+            this._startY = this._top = (downPt[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
+        }
 
         const doc = this.props.iframe?.()?.contentDocument ?? document;
+        doc.removeEventListener('pointermove', this.onSelectMove);
+        doc.removeEventListener('pointerup', this.onSelectEnd);
         doc.addEventListener('pointermove', this.onSelectMove);
         doc.addEventListener('pointerup', this.onSelectEnd);
 
-        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.OnAudio = unimplementedFunction;
-        AnchorMenu.Instance.Highlight = this.highlight;
-        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
-        AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
-
         /**
          * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
          * It also initiates a Drag/Drop interaction to place the text annotation.
@@ -125,7 +133,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                   });
               });
     }
-    componentWillUnmount() {
+    releaseDownPt() {
         const doc = this.props.iframe?.()?.contentDocument ?? document;
         doc.removeEventListener('pointermove', this.onSelectMove);
         doc.removeEventListener('pointerup', this.onSelectEnd);
@@ -259,6 +267,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                 this.highlight('rgba(245, 230, 95, 0.75)', false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color)
             }
             this.props.finishMarquee(undefined, undefined, e);
+            runInAction(() => (this._width = this._height = 0));
         } else {
             runInAction(() => (this._width = this._height = 0));
             this.props.finishMarquee(cliX, cliY, e);
@@ -266,8 +275,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
     };
 
     render() {
-        return (
+        return !this.props.down ? null : (
             <div
+                ref={r => (r ? this.gotDownPoint() : this.releaseDownPt())}
                 className="marqueeAnnotator-dragBox"
                 style={{
                     left: `${this._left}px`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b66dc0aa2..4f3f21cea 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -172,8 +172,6 @@ export function CollectionSubView<X>(moreProps?: X) {
                 // The following conditional detects a recurring bug we've seen on the server
                 if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) {
                     alert('COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...');
-                    console.log(doc);
-                    console.log(proto);
                     throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
                 }
                 let cursors = Cast(proto.cursors, listSpec(CursorField));
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e45e2a1cb..cd58319cb 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -247,8 +247,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 alert('Cannot pin presentation document to itself');
                 return;
             }
-            const pinDoc = Doc.MakeAlias(doc);
-            pinDoc.presentationTargetDoc = doc;
+            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.();
+            const pinDoc = Doc.MakeAlias(anchorDoc ?? doc);
+            pinDoc.presentationTargetDoc = anchorDoc ?? 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 = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 82b97dff0..dc0eb69f3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1146,6 +1146,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset
     }
 
+    _focusCount = 0;
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
         const state = HistoryUtil.getState();
 
@@ -1187,6 +1188,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             }
 
+            const focusCount = ++this._focusCount;
             const startTime = Date.now();
             // focus on this collection within its parent view.  the parent view after focusing determines whether to reset the view change within the collection
             const endFocus = async (moved: boolean) => {
@@ -1200,7 +1202,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         this.Document[this.scaleFieldKey] = restoreState.scale;
                     }
                 }
-                runInAction(() => (this._panZoomTransition = 0));
+                this._focusCount === focusCount && didMove && runInAction(() => (this._panZoomTransition = 0));
                 return resetView;
             };
             const xf = !cantTransform
@@ -2284,5 +2286,8 @@ ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
     runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
 });
 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()) }));
+    !readOnly &&
+        SelectionManager.Views().forEach(view =>
+            TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) })
+        );
 });
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3ab7cc0bc..569579996 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -59,7 +59,7 @@ class ObserverJsxParser1 extends JsxParser {
     }
 }
 
-const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
+export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
 
 interface HTMLtagProps {
     Document: Doc;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index abf6e37ab..453bdac8e 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -51,6 +51,22 @@
         height: calc(100% - 20px);
     }
 
+    .documentView-htmlOverlay {
+        position: absolute;
+        display: flex;
+        top: 0;
+        height: 100%;
+        width: 100%;
+        .documentView-htmlOverlayInner {
+            box-shadow: black 0.2vw 0.2vw 0.8vw;
+            background: rgb(255, 255, 255);
+            overflow: auto;
+            position: relative;
+            margin: auto;
+            padding: 20px;
+        }
+    }
+
     .documentView-linkAnchorBoxAnchor {
         display: flex;
         overflow: hidden;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 76cc6800a..95cf08289 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
+import { ObserverJsxParser } from './DocumentContentsView';
 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';
@@ -110,6 +111,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
+    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
     easeFunc?: 'linear' | 'ease'; // transition method for scrolling
@@ -590,9 +592,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
         const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false });
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
+        const startTime = Date.now();
         this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
             ...options,
-            afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)),
+            afterFocus: (didFocus: boolean) =>
+                new Promise<ViewAdjustment>(async res =>
+                    setTimeout(
+                        async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), //
+                        didFocus ? Math.max(0, (options.zoomTime ?? 500) - (Date.now() - startTime)) : 0
+                    )
+                ),
         });
     };
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
@@ -1098,11 +1107,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     };
     @observable _retryThumb = 1;
     thumbShown = () => {
+        const childHighlighted = () =>
+            Array.from(Doc.highlightedDocs.keys())
+                .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() &&
             LightboxView.LightboxDoc !== this.rootDoc &&
             this.thumb &&
             !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
-            (!Doc.isBrushedHighlightedDegree(this.props.Document) || this.rootDoc._viewType === CollectionViewType.Docking) &&
+            ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) &&
             !this._componentView?.isAnyChildContentActive?.()
             ? true
             : false;
@@ -1754,6 +1768,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
 
+    @observable textHtmlOverlay: Opt<string>;
     @computed get anchorViewDoc() {
         return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined;
     }
@@ -1793,6 +1808,24 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     isHovering = () => this._isHovering;
     @observable _isHovering = false;
 
+    htmlOverlayEffect = '';
+    @computed get htmlOverlay() {
+        const effectProps = {
+            delay: 0,
+            duration: 500,
+        };
+        const highlight = (
+            <div className="webBox-textHighlight">
+                <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
+            </div>
+        );
+        return !this.textHtmlOverlay ? null : (
+            <div className="documentView-htmlOverlay">
+                <div className="documentView-htmlOverlayInner">{<Fade {...effectProps}>{DocumentViewInternal.AnimationEffect(highlight, { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc)} </Fade>}</div>
+            </div>
+        );
+    }
+
     render() {
         TraceMobx();
         const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
@@ -1841,6 +1874,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                             focus={this.props.focus || emptyFunction}
                             ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))}
                         />
+                        {this.htmlOverlay}
                     </div>
                 )}
 
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index b0f6f8358..c8d5b0154 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -322,7 +322,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
      */
     sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
         console.log('print all sidebar Docs');
-        console.log(this.allSidebarDocs);
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
         const docs = doc instanceof Doc ? [doc] : doc;
         docs.forEach(doc => {
@@ -337,8 +336,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 }
             }
         }); //add to annotation list
-        console.log('sidebaraddDocument');
-        console.log(doc);
 
         return this.addDocument(doc, sidebarKey); // add to sidebar list
     };
@@ -352,10 +349,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
         if (this.layoutDoc._showSidebar) this.toggleSidebar();
         const docs = doc instanceof Doc ? [doc] : doc;
-        docs.forEach(doc => {
-            console.log(this.allMapMarkers);
-            console.log(this.allSidebarDocs);
-        });
         return this.removeDocument(doc, sidebarKey);
     };
 
@@ -405,7 +398,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
      */
     @action
     private handlePlaceChanged = () => {
-        console.log(this.searchBox);
         const place = this.searchBox.getPlace();
 
         if (!place.geometry || !place.geometry.location) {
@@ -416,7 +408,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
         // zoom in on the location of the search result
         if (place.geometry.viewport) {
-            console.log(this._map);
             this._map.fitBounds(place.geometry.viewport);
         } else {
             this._map.setCenter(place.geometry.location);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 88aac67a7..e22ee5021 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -213,6 +213,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
+        let ele: Opt<HTMLDivElement> = undefined;
+        if (this._pdfViewer?.selectionContent()) {
+            ele = document.createElement('div');
+            ele.append(this._pdfViewer.selectionContent()!);
+        }
         const docAnchor = () => {
             const anchor = Docs.Create.TextanchorDocument({
                 title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
@@ -222,6 +227,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             return anchor;
         };
         const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? docAnchor();
+        anchor.text = ele?.textContent ?? '';
+        anchor.textHtml = ele?.innerHTML;
         this.addDocument(anchor);
         return anchor;
     };
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a41f66ef0..6f578a9fc 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -7,6 +7,7 @@
     left: 0;
     position: relative;
     display: flex;
+    overflow: hidden;
 
     .webBox-sideResizer {
         position: absolute;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a3e83f047..8be4884ce 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -29,7 +29,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
@@ -54,10 +54,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _keyInput = React.createRef<HTMLInputElement>();
     private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop));
-    private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     private _sidebarRef = React.createRef<SidebarAnnos>();
     private _searchRef = React.createRef<HTMLInputElement>();
     private _searchString = '';
+
+    private get _getAnchor() {
+        return AnchorMenu.Instance?.GetAnchor;
+    }
     @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render.
     @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe.  so thisforces the ifrmae to be rebuilt when allowScripts is toggled
     @observable private _searching: boolean = false;
@@ -285,7 +288,7 @@ 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 (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.instant);
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
             this.toggleSidebar(options.instant);
         }
@@ -305,6 +308,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     getAnchor = () => {
+        let ele: Opt<HTMLDivElement> = undefined;
+        try {
+            const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
+            if (contents) {
+                ele = document.createElement('div');
+                ele.append(contents);
+            }
+        } catch (e) {}
         const anchor =
             this._getAnchor(this._savedAnnotations) ??
             Docs.Create.WebanchorDocument(this._url, {
@@ -312,6 +323,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 y: NumCast(this.layoutDoc._scrollTop),
                 unrendered: true,
             });
+        anchor.text = ele?.textContent ?? '';
+        anchor.textHtml = ele?.innerHTML;
         this.addDocumentWrapper(anchor);
         return anchor;
     };
@@ -553,8 +566,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     static urlHash = (s: string) => {
+        const split = s.split('');
         return Math.abs(
-            s.split('').reduce((a: any, b: any) => {
+            split.reduce((a: any, b: any) => {
                 a = (a << 5) - a + b.charCodeAt(0);
                 return a & a;
             }, 0)
@@ -686,7 +700,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
     };
     @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => {
-        this._getAnchor = AnchorMenu.Instance?.GetAnchor;
         this._marqueeing = undefined;
         this._isAnnotating = false;
         this._iframeClick = undefined;
@@ -1006,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 selectionText={returnEmptyString}
                                 annotationLayer={this._annotationLayer.current}
                                 mainCont={this._mainCont.current}
-                            />{' '}
+                            />
                         </div>
                     )}
                 </div>
@@ -1043,5 +1056,5 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     }
 }
 ScriptingGlobals.add(function urlHash(url: string) {
-    return WebBox.urlHash(url);
+    return url ? WebBox.urlHash(url) : 0;
 });
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index b26f485be..2e3135416 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -65,7 +65,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _obDisposers: { [name: string]: any } = {};
     public selectedArray = new ObservableSet<Doc>();
 
     @observable public static Instance: PresBox;
@@ -142,20 +141,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // Turn of progressivize editors
         this.turnOffEdit(true);
         Object.values(this._disposers).forEach(disposer => disposer?.());
-        Object.values(this._obDisposers).forEach(disposer => disposer?.());
     }
 
     @action
     componentDidMount() {
-        this._obDisposers.anim = observe(
-            this,
-            'activeItem',
-            change => {
-                change.oldValue && (DocCast((change.oldValue as Doc).presentationTargetDoc)[AnimationSym] = undefined);
-                change.newValue && (DocCast((change.newValue as Doc).presentationTargetDoc)[AnimationSym] = change.newValue as Doc);
-            },
-            true
-        );
         this._disposers.keyboard = reaction(
             () => this.selectedDoc,
             selected => {
@@ -217,16 +206,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // 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 = (slideNum?: number) => {
-        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
-        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) {
-                this.gotoDocument(nextSelected, this.activeItem, true);
-            } else {
-                break;
+        const nextSlideInd = slideNum ?? this.itemIndex + 1;
+        let curSlideInd = nextSlideInd;
+        const resetSelection = action(() => {
+            this.clearSelectedArray();
+            for (let i = nextSlideInd; i <= curSlideInd; i++) {
+                this.addToSelectedArray(this.childDocs[i]);
             }
-        }
+        });
+        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
+        this.clearSelectedArray();
+        const doGroupWithUp =
+            (nextSelected: number, force = false) =>
+            () => {
+                if (nextSelected < this.childDocs.length) {
+                    if (force || this.childDocs[nextSelected].groupWithUp) {
+                        const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].groupWithUp) > 1;
+                        if (serial) {
+                            this.gotoDocument(nextSelected, this.activeItem, true, async () => {
+                                const waitTime = NumCast(this.activeItem.presDuration) - NumCast(this.activeItem.presTransition);
+                                await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime)));
+                                doGroupWithUp(nextSelected + 1)();
+                            });
+                        } else {
+                            this.gotoDocument(nextSelected, this.activeItem, undefined, resetSelection);
+                            curSlideInd = this.itemIndex;
+                            doGroupWithUp(nextSelected + 1)();
+                        }
+                    }
+                }
+            };
+        doGroupWithUp(curSlideInd, true)();
     };
 
     // Called when the user activates 'next' - to move to the next part of the pres. trail
@@ -270,9 +280,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //The function that is called when a document is clicked or reached through next or back.
     //it'll also execute the necessary actions if presentation is playing.
     @undoBatch
-    public gotoDocument = action((index: number, from?: Doc, group?: boolean) => {
+    public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => {
         Doc.UnBrushAllDocs();
-
         if (index >= 0 && index < this.childDocs.length) {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
@@ -280,10 +289,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
             if (activeFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
-                const context = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
+                const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
+                const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
                 if (context) {
                     const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
-                    if (ffview) {
+                    if (ffview?.childDocs) {
                         this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
                         context._currentFrame = NumCast(activeFrame);
                     }
@@ -302,7 +312,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
             this.turnOffEdit();
-            this.navigateToActiveItem(); //Handles movement to element only when presTrail is list
+            this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list
             this.onHideDocument(); //Handles hide after/before
         }
     });
@@ -489,13 +499,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * a new tab. If presCollection is undefined it will open the document
      * on the right.
      */
-    navigateToActiveItem = () => {
+    navigateToActiveItem = (afterNav?: () => void) => {
         const activeItem: Doc = this.activeItem;
         const targetDoc: Doc = this.targetDoc;
+        const finished = () => {
+            afterNav?.();
+            console.log('Finish Slide Nav: ' + targetDoc.title);
+            targetDoc[AnimationSym] = undefined;
+        };
         const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null);
         const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
         const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined;
-        const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc);
+        const includesDoc: boolean = DocumentManager.Instance.getDocumentView(targetDoc) ? true : false; //   DocListCast(presCollection?.data).includes(targetDoc);
         const tabMap = CollectionDockingView.Instance?.tabMap;
         const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc === srcContext || tab.DashDoc === targetDoc);
         // Handles the setting of presCollection
@@ -517,13 +532,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             selViewCache.forEach(doc => this.addToSelectedArray(doc));
             this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
             this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
+            finished();
         });
         const createDocView = (doc: Doc, finished?: () => void) => {
             DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
             (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox);
             this.layoutDoc.presCollection = targetDoc;
         };
-        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? undefined : resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? finished : resetSelection);
     };
 
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) {
@@ -552,11 +568,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
                     zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1),
                     zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                    effect: activeItem,
                     noSelect: true,
                     originatingDoc: activeItem,
                     easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
+                    zoomTextSelections: true
                 };
-
+                if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
                 var containerDocContext = srcContext ? [srcContext] : [];
                 while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
                     containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
@@ -577,12 +595,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.childDocs.forEach((doc, index) => {
             const curDoc = Cast(doc, Doc, null);
             const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
-            //if (tagDoc) tagDoc.opacity = 1;
             const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
-            const curInd: number = itemIndexes.indexOf(index);
             if (tagDoc === this.layoutDoc.presCollection) {
                 tagDoc.opacity = 1;
             } else {
+                if (curDoc.presHide) {
+                    if (index !== this.itemIndex) {
+                        tagDoc.opacity = 1;
+                    }
+                }
                 const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex);
                 if (curDoc.presHideBefore && index === hidingIndBef) {
                     if (index > this.itemIndex) {
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 4469be488..5e1474b89 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -507,11 +507,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
                     <div
                         className="slideButton"
-                        onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
+                        onClick={() => (activeItem.groupWithUp = (NumCast(activeItem.groupWithUp) + 1) % 3)}
                         style={{
                             zIndex: 1000 - this.indexInPres,
                             fontWeight: 700,
                             backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
+                            outline: NumCast(activeItem.groupWithUp) > 1 ? 'solid black 1px' : undefined,
                             height: activeItem.groupWithUp ? 53 : 18,
                             transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
                         }}>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 906dff377..f95d5ac2e 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -68,9 +68,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
-    public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _selectionText: string = '';
+    private _selectionContent: DocumentFragment | undefined;
     private _downX: number = 0;
     private _downY: number = 0;
     private _lastSearch = false;
@@ -78,8 +78,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _ignoreScroll = false;
     private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined;
     private _forcedScroll = true;
+    get _getAnchor() {
+        return AnchorMenu.Instance?.GetAnchor;
+    }
 
     selectionText = () => this._selectionText;
+    selectionContent = () => this._selectionContent;
 
     @observable isAnnotating = false;
     // key where data is stored
@@ -392,7 +396,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     finishMarquee = (x?: number, y?: number) => {
-        this._getAnchor = AnchorMenu.Instance?.GetAnchor;
         this.isAnnotating = false;
         this._marqueeing = undefined;
         this._textSelecting = true;
@@ -437,7 +440,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 }
             }
         }
-        this._selectionText = selRange.cloneContents().textContent || '';
+        this._selectionContent = selRange.cloneContents();
+        this._selectionText = this._selectionContent?.textContent || '';
 
         // clear selection
         if (sel.empty) {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6762665a2..19ffc5005 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { saveAs } from 'file-saver';
-import { action, computed, observable, ObservableMap, runInAction } from 'mobx';
+import { action, computed, observable, ObservableMap, ObservableSet, runInAction } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { alias, map, serializable } from 'serializr';
 import { DocServer } from '../client/DocServer';
@@ -343,6 +343,10 @@ export class Doc extends RefField {
     @observable public [DirectLinksSym]: Set<Doc> = new Set();
     @observable public [AnimationSym]: Opt<Doc>;
     @observable public [HighlightSym]: boolean = false;
+    static __Anim(Doc: Doc) {
+        // for debugging to print AnimationSym field easily.
+        return Doc[AnimationSym];
+    }
 
     private [UpdatingFromServer]: boolean = false;
     private [ForceServerWrite]: boolean = false;
@@ -1131,7 +1135,7 @@ export namespace Doc {
         BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
         SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
     }
-    const brushManager = new DocBrush();
+    export const brushManager = new DocBrush();
 
     export class DocData {
         @observable _user_doc: Doc = undefined!;
@@ -1284,8 +1288,8 @@ export namespace Doc {
     }
 
     let UnhighlightWatchers: (() => void)[] = [];
-    let UnhighlightTimer: any;
-    export function AddUnlightWatcher(watcher: () => void) {
+    export let UnhighlightTimer: any;
+    export function AddUnHighlightWatcher(watcher: () => void) {
         if (UnhighlightTimer) {
             UnhighlightWatchers.push(watcher);
         } else watcher();
@@ -1302,16 +1306,16 @@ export namespace Doc {
         }, 5000);
     }
 
-    var highlightedDocs = new Set<Doc>();
+    export var highlightedDocs = new ObservableSet<Doc>();
     export function IsHighlighted(doc: Doc) {
         if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
         return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
     }
     export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
         runInAction(() => {
-            doc[AnimationSym] = presEffect;
             highlightedDocs.add(doc);
             doc[HighlightSym] = true;
+            doc[AnimationSym] = presEffect;
             if (dataAndDisplayDocs) {
                 highlightedDocs.add(Doc.GetProto(doc));
                 Doc.GetProto(doc)[HighlightSym] = true;
-- 
cgit v1.2.3-70-g09d2


From 84f728cffb94319b86be8d6cc478ce424ec45c2f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 20 Jan 2023 11:17:38 -0500
Subject: removed tour map from lightbox.  added option to create anchors
 without adding thm as annotations. made zoom of text an option for pres and
 links

---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/DragManager.ts                     |  4 +-
 src/client/util/LinkFollower.ts                    | 10 +---
 src/client/views/DocComponent.tsx                  |  7 +++
 src/client/views/DocumentButtonBar.tsx             |  2 +-
 src/client/views/InkingStroke.tsx                  |  4 +-
 src/client/views/LightboxView.tsx                  | 56 +++++-----------------
 src/client/views/MarqueeAnnotator.tsx              |  8 ++--
 src/client/views/PropertiesView.tsx                | 10 ++++
 .../collections/CollectionStackedTimeline.tsx      | 18 +++----
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 .../views/collections/CollectionTimeView.tsx       | 14 +++---
 src/client/views/collections/TabDocView.tsx        |  2 +-
 .../CollectionFreeFormLinkView.tsx                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 12 +++--
 src/client/views/nodes/AudioBox.tsx                |  7 ++-
 src/client/views/nodes/DocumentLinksButton.tsx     |  6 +--
 src/client/views/nodes/DocumentView.tsx            | 14 +++---
 src/client/views/nodes/FunctionPlotBox.tsx         |  7 +--
 src/client/views/nodes/ImageBox.tsx                |  8 ++--
 src/client/views/nodes/LabelBox.tsx                |  4 +-
 src/client/views/nodes/LinkDocPreview.tsx          |  2 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |  5 +-
 src/client/views/nodes/PDFBox.tsx                  |  8 ++--
 src/client/views/nodes/ScreenshotBox.tsx           |  4 +-
 src/client/views/nodes/VideoBox.tsx                | 11 +++--
 src/client/views/nodes/WebBox.tsx                  |  6 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 39 +++++++--------
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 src/client/views/nodes/trails/PresBox.tsx          | 13 +++--
 src/client/views/pdf/AnchorMenu.tsx                | 13 ++---
 src/client/views/pdf/Annotation.tsx                |  8 ++--
 src/client/views/pdf/PDFViewer.tsx                 |  4 +-
 33 files changed, 157 insertions(+), 159 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b51fcb454..80b040cc0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1310,13 +1310,13 @@ export namespace DocUtils {
         options?.afterFocus?.(false);
     }
 
-    export let ActiveRecordings: { props: FieldViewProps; getAnchor: () => Doc }[] = [];
+    export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
 
     export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
         broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
         return DocUtils.ActiveRecordings.map(audio => {
             const sourceDoc = getSourceDoc();
-            const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
+            const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor(true) || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
             link && (link.followLinkLocation = OpenWhere.addRight);
             return link;
         });
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index d0690fa10..a56f87075 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -251,8 +251,8 @@ export namespace DragManager {
     }
 
     // drags a linker button and creates a link on drop
-    export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | (() => Doc), downX: number, downY: number, options?: DragOptions) {
-        StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.() ?? sourceView.rootDoc), downX, downY, options);
+    export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | ((addAsAnnotation: boolean) => Doc), downX: number, downY: number, options?: DragOptions) {
+        StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.(true) ?? sourceView.rootDoc), downX, downY, options);
     }
 
     // drags a column from a schema view
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 0f216e349..5bdfca54a 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -111,15 +111,9 @@ export class LinkFollower {
                         easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
                         effect: sourceDoc,
                         originatingDoc: sourceDoc,
-                        zoomTextSelections: false,
+                        zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText),
                     };
-                    if (target.TourMap) {
-                        const fieldKey = Doc.LayoutFieldKey(target);
-                        const tour = DocListCast(target[fieldKey]).reverse();
-                        LightboxView.SetLightboxDoc(currentContext, undefined, tour);
-                        setTimeout(LightboxView.Next);
-                        allFinished();
-                    } else if (target.type === DocumentType.PRES) {
+                    if (target.type === DocumentType.PRES) {
                         const containerAnnoDoc = Cast(sourceDoc, Doc, null);
                         const containerDoc = containerAnnoDoc || sourceDoc;
                         var containerDocContext = containerDoc?.context ? [Cast(await containerDoc?.context, Doc, null)] : ([] as Doc[]);
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index c9c09b63b..78ab2b3d4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -130,6 +130,13 @@ 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/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 90c6c040c..f06dc93e3 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -501,7 +501,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                             key="popup"
                             showPopup={this._showLinkPopup}
                             linkCreated={link => (link.linkDisplay = !this.props.views().lastElement()?.rootDoc.isLinkButton)}
-                            linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.()}
+                            linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.(true)}
                             linkFrom={() => this.props.views().lastElement()?.rootDoc}
                         />
                     </div>
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a6fa2f04b..319a9419e 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -78,8 +78,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
     // fit within its panel (e.g., for content fitting views like Lightbox or multicolumn, etc)
     screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1);
 
-    getAnchor = () => {
-        return this._subContentView?.getAnchor?.() || this.rootDoc;
+    getAnchor = (addAsAnnotation: boolean) => {
+        return this._subContentView?.getAnchor?.(addAsAnnotation) || this.rootDoc;
     };
 
     scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index d79b696a3..3627aa783 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -37,7 +37,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
     @observable private static _doc: Opt<Doc>;
     @observable private static _docTarget: Opt<Doc>;
     @observable private static _docFilters: string[] = []; // filters
-    @observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target
     private static _savedState: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }>;
     private static _history: Opt<{ doc: Doc; target?: Doc }[]> = [];
     @observable private static _future: Opt<Doc[]> = [];
@@ -90,13 +89,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         this._doc = doc;
         this._layoutTemplate = layoutTemplate;
         this._docTarget = target || doc;
-        this._tourMap = DocListCast(doc?.links)
-            .map(link => {
-                const opp = LinkManager.getOppositeAnchor(link, doc!);
-                return opp?.TourMap ? opp : undefined;
-            })
-            .filter(m => m)
-            .map(m => m!);
 
         return true;
     }
@@ -164,7 +156,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         const target = (LightboxView._docTarget = this._future?.pop());
         const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
         if (targetDocView && target) {
-            const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
+            const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
             targetDocView.focus(target, { originalTarget: target, willPanZoom: true, zoomScale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
@@ -189,13 +181,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 LightboxView.SetLightboxDoc(target);
             }
         }
-        LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links)
-            .map(link => {
-                const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!);
-                return opp?.TourMap ? opp : undefined;
-            })
-            .filter(m => m)
-            .map(m => m!);
     }
 
     @action public static Previous() {
@@ -214,13 +199,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             LightboxView.SetLightboxDoc(doc, target);
         }
         if (LightboxView._future?.lastElement() !== previous.target || previous.doc) LightboxView._future?.push(previous.target || previous.doc);
-        LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links)
-            .map(link => {
-                const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!);
-                return opp?.TourMap ? opp : undefined;
-            })
-            .filter(m => m)
-            .map(m => m!);
     }
     @action
     stepInto = () => {
@@ -231,27 +209,20 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             history: LightboxView._history,
             saved: LightboxView._savedState,
         });
-        const tours = LightboxView._tourMap;
-        if (tours && tours.length) {
-            const fieldKey = Doc.LayoutFieldKey(tours[0]);
-            LightboxView._future?.push(...DocListCast(tours[0][fieldKey]).reverse());
-        } else {
-            const coll = LightboxView._docTarget;
-            if (coll) {
-                const fieldKey = Doc.LayoutFieldKey(coll);
-                const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])];
-                const links = DocListCast(coll.links)
-                    .map(link => LinkManager.getOppositeAnchor(link, coll))
-                    .filter(doc => doc)
-                    .map(doc => doc!);
-                LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
-                TabDocView.PinDoc(coll, { hidePresBox: true });
-            }
+        const coll = LightboxView._docTarget;
+        if (coll) {
+            const fieldKey = Doc.LayoutFieldKey(coll);
+            const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])];
+            const links = DocListCast(coll.links)
+                .map(link => LinkManager.getOppositeAnchor(link, coll))
+                .filter(doc => doc)
+                .map(doc => doc!);
+            LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
+            TabDocView.PinDoc(coll, { hidePresBox: true });
         }
     };
 
     future = () => LightboxView._future;
-    tourMap = () => LightboxView._tourMap;
     render() {
         let downx = 0,
             downy = 0;
@@ -345,7 +316,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     },
                     this.future()?.length.toString()
                 )}
-                <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} />
+                <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} />
                 <div
                     className="lightboxView-navBtn"
                     title={'toggle fit width'}
@@ -393,7 +364,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
 }
 interface LightboxTourBtnProps {
     navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element;
-    tourMap: () => Opt<Doc[]>;
     future: () => Opt<Doc[]>;
     stepInto: () => void;
 }
@@ -410,7 +380,7 @@ export class LightboxTourBtn extends React.Component<LightboxTourBtnProps> {
                 e.stopPropagation();
                 this.props.stepInto();
             },
-            StrCast(this.props.tourMap()?.lastElement()?.TourMap)
+            ''
         );
     }
 }
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index bf1242346..b1965c5c8 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -52,8 +52,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true));
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = this.highlight;
-        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
-        AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
+        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, addAsAnnotation);
+        AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
     }
 
     @action
@@ -194,11 +194,11 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         return textRegionAnno;
     };
     @action
-    highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => {
+    highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
         // creates annotation documents for current highlights
         const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]);
         const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
-        !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
+        addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
         return (annotationDoc as Doc) ?? undefined;
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 8d495d286..af50478b0 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1679,6 +1679,16 @@ 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>Zoom Text Selections</p>
+                                            <button
+                                                style={{ background: !this.sourceAnchor?.followLinkZoomText ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', this.sourceAnchor)}
+                                                onClick={e => e.stopPropagation()}
+                                                className="propertiesButton">
+                                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                            </button>
+                                        </div>
                                         <div className="propertiesView-input inline">
                                             <p>Toggle Target (Show/Hide)</p>
                                             <button
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index e9bf03208..b7ea26b44 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -190,7 +190,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                         CollectionStackedTimeline.SelectingRegion = this;
                     } else {
                         this._markerEnd = this.currentTime;
-                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd);
+                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd, undefined, true);
                         this._markerEnd = undefined;
                         CollectionStackedTimeline.SelectingRegion = undefined;
                     }
@@ -257,7 +257,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                         this._markerEnd = tmp;
                     }
                     if (!isClick && Math.abs(movement[0]) > 15 && !this.IsTrimming) {
-                        const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd);
+                        const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd, undefined, true);
                         setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
                     }
                     (!isClick || !wasSelecting) && (this._markerEnd = undefined);
@@ -273,7 +273,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                 undefined,
                 () => {
                     if (shiftKey) {
-                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime);
+                        CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime, undefined, undefined, true);
                     } else {
                         !wasPlaying && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
                     }
@@ -388,7 +388,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
     // creates marker on timeline
     @undoBatch
     @action
-    static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number, docAnchor?: Doc) {
+    static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime: Opt<number>, anchorEndTime: Opt<number>, docAnchor: Opt<Doc>, addAsAnnotation: boolean) {
         if (anchorStartTime === undefined) return rootDoc;
         const anchor =
             docAnchor ??
@@ -407,10 +407,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
             });
         Doc.GetProto(anchor)[startTag] = anchorStartTime;
         Doc.GetProto(anchor)[endTag] = anchorEndTime;
-        if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
-            Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
-        } else {
-            dataDoc[fieldKey] = new List<Doc>([anchor]);
+        if (addAsAnnotation) {
+            if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
+                Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
+            } else {
+                dataDoc[fieldKey] = new List<Doc>([anchor]);
+            }
         }
         return anchor;
     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index d2074219a..27ae3041f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -334,7 +334,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                                 const iframe = SelectionManager.Views()[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
                                 const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
                                 if (focusNode) {
-                                    const anchor = srcWeb?.ComponentView?.getAnchor?.();
+                                    const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
                                     anchor && DocUtils.MakeLink({ doc: htmlDoc }, { doc: anchor });
                                 }
                             }
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index a1466bcd0..8c613198d 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -32,7 +32,7 @@ export class CollectionTimeView extends CollectionSubView() {
     @observable _viewDefDivClick: Opt<ScriptField>;
     @observable _focusPivotField: Opt<string>;
 
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.HTMLAnchorDocument([], {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
             annotationOn: this.rootDoc,
@@ -45,11 +45,13 @@ export class CollectionTimeView extends CollectionSubView() {
         proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]);
         proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
 
-        // store anchor in annotations list of document (not technically needed since these anchors are never drawn)
-        if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
-            Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
-        } else {
-            this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
+        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
+            if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
+                Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
+            } else {
+                this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
+            }
         }
         return anchor;
     };
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5c05b5c13..f6e4b45ee 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -247,7 +247,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 alert('Cannot pin presentation document to itself');
                 return;
             }
-            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.();
+            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false);
             const pinDoc = Doc.MakeAlias(anchorDoc ?? doc);
             pinDoc.presentationTargetDoc = anchorDoc ?? doc;
             pinDoc.title = doc.title + ' - Slide';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 8919b1c01..9811c239b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -290,7 +290,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     stroke="antiquewhite"
                     strokeWidth="4"
                     className="collectionfreeformlinkview-linkLine"
-                    style={{ pointerEvents: 'all', opacity: this._opacity, stroke, strokeWidth }}
+                    style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }}
                     onClick={this.onClickLine}
                     d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
                     markerEnd={link.linkDisplayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index dc0eb69f3..5cf4cb31f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1565,7 +1565,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             : undefined;
     }; // 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 = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         if (this.props.Document.annotationOn) {
             return this.rootDoc;
         }
@@ -1574,10 +1574,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const proto = Doc.GetProto(anchor);
         proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
         proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
-        if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
-            Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
-        } else {
-            this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
+        if (addAsAnnotation) {
+            if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
+                Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
+            } else {
+                this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
+            }
         }
         return anchor;
     };
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index d95668c89..1d59d3356 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -136,7 +136,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return { la1, la2, linkTime };
     }
 
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         return (
             CollectionStackedTimeline.createAnchor(
                 this.rootDoc,
@@ -144,7 +144,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 this.annotationKey,
                 '_timecodeToShow' /* audioStart */,
                 '_timecodeToHide' /* audioEnd */,
-                this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)
+                this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
+                undefined,
+                undefined,
+                addAsAnnotation
             ) || this.rootDoc
         );
     };
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 6f3152981..a39e0f65f 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -135,7 +135,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                             DocumentLinksButton.AnnotationId = undefined;
                         } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) {
                             const sourceDoc = DocumentLinksButton.StartLink;
-                            const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document;
+                            const targetDoc = this.props.View.ComponentView?.getAnchor?.(true) || this.props.View.Document;
                             const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, 'links'); //why is long drag here when this is used for completing links by clicking?
 
                             LinkManager.currentLink = linkDoc;
@@ -183,8 +183,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                 DocumentLinksButton.AnnotationUri = undefined;
                 //!this.props.StartLink
             } else if (startLink !== endLink) {
-                endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink;
-                startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink;
+                endLink = endLinkView?.docView?._componentView?.getAnchor?.(true) || endLink;
+                startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
                 const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
 
                 LinkManager.currentLink = linkDoc;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1df46488b..054d01d8e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -121,7 +121,7 @@ export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
 export interface DocComponentView {
     updateIcon?: () => void; // updates the icon representation of the document
-    getAnchor?: () => 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)
+    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
@@ -783,9 +783,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => {
+    toggleFollowLink = (location: Opt<string>, zoom?: boolean, setTargetToggle?: boolean): void => {
         this.Document.ignoreClick = false;
-        if (setPushpin) {
+        if (setTargetToggle) {
             this.Document.followLinkToggle = !this.Document.followLinkToggle;
             this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton;
         } else {
@@ -854,7 +854,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
             }
             if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
-                const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document;
+                const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document;
                 de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
             }
         }
@@ -979,7 +979,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, false), icon: 'link' });
                     !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' });
                     onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' });
-                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
+                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
                     onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
                 } else if (DocListCast(this.Document.links).length) {
@@ -1091,7 +1091,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
     onClickFunc = () => this.onClickHandler;
     setHeight = (height: number) => (this.layoutDoc._height = height);
-    setContentView = action((view: { getAnchor?: () => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
+    setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
     isContentActive = (outsideReaction?: boolean) => {
         return this.props.isContentActive() === false
             ? false
@@ -1264,7 +1264,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     playAnnotation = () => {
         const self = this;
         const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
-        const anno = audioAnnos.lastElement();
+        const anno = audioAnnos?.lastElement();
         if (anno instanceof AudioField && this.audioAnnoState === 'stopped') {
             new Howl({
                 src: [anno.url.href],
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 24562ccbd..5c0005dae 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -12,7 +12,7 @@ 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 { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { DocFocusOptions } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 
@@ -22,7 +22,7 @@ type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSch
 const EquationDocument = makeInterface(EquationSchema, documentSchema);
 
 @observer
-export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
+export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(FunctionPlotBox, fieldKey);
     }
@@ -41,10 +41,11 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
             () => this.createGraph()
         );
     }
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc });
         anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
         anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
+        if (addAsAnnotation) this.addDocument(anchor);
         return anchor;
     };
     @action
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ac953d13b..e2ecca0b6 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -52,7 +52,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+    private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
     @observable _curSuffix = '';
     @observable _uploadIcon = uploadIcons.idle;
 
@@ -86,13 +86,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             : undefined;
     }; // 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 = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         const anchor =
-            this._getAnchor?.(this._savedAnnotations) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
+            this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
             Docs.Create.ImageanchorDocument({ presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
         if (anchor) {
             PresBox.pinDocView(anchor, { pinData: { pannable: true, dataview: true, dataannos: true } }, this.rootDoc);
-            this.addDocument(anchor);
+            addAsAnnotation && this.addDocument(anchor);
             return anchor;
         }
         return this.rootDoc;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 10897b48f..a2143f629 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -36,9 +36,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
         this._timeout && clearTimeout(this._timeout);
     }
 
-    getAnchor = () => {
-        return this.rootDoc;
-    };
+    getAnchor = (addAsAnnotation: boolean) => this.rootDoc;
 
     getTitle() {
         return this.rootDoc['title-custom'] ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 232c3459e..7eb42994b 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -113,7 +113,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                     this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                                 }
                                 this._toolTipText = '';
-                                if (LinkDocPreview.LinkInfo?.noPreview || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
+                                if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
                             }
                         })
                     );
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c8d5b0154..95cb49037 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -522,10 +522,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         );
     }
 
-    getAnchor = () => {
-        const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? this.rootDoc;
-        return anchor;
-    };
+    getAnchor = (addAsAnnotation: boolean) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
 
     /**
      * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index e22ee5021..71ef9bc84 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -212,7 +212,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         PresBox.restoreTargetDocView(this.props.DocumentView?.(), {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
         return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
     };
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         let ele: Opt<HTMLDivElement> = undefined;
         if (this._pdfViewer?.selectionContent()) {
             ele = document.createElement('div');
@@ -226,10 +226,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc);
             return anchor;
         };
-        const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? docAnchor();
+        const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), false) ?? docAnchor();
         anchor.text = ele?.textContent ?? '';
         anchor.textHtml = ele?.innerHTML;
-        this.addDocument(anchor);
+        if (addAsAnnotation) {
+            this.addDocument(anchor);
+        }
         return anchor;
     };
 
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 76a24d831..f94996c66 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -128,9 +128,9 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
             this.setupDictation();
         }
     }
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         const startTime = Cast(this.layoutDoc._currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow', '_timecodeToHide', startTime, startTime === undefined ? undefined : startTime + 3) || this.rootDoc;
+        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow', '_timecodeToHide', startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc;
     };
 
     videoLoad = () => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a8f78edd5..1dfa55c64 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -385,15 +385,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
         Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
         this.props.addDocument?.(imageSummary);
-        const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, 'video snapshot');
+        const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor(true) }, 'video snapshot');
         link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
         setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
     };
 
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
-        const marquee = AnchorMenu.Instance.GetAnchor?.();
-        return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+        const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
+        return (
+            CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) ||
+            this.rootDoc
+        );
     };
 
     // sets video info on load
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 8be4884ce..acf4fe4b0 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -307,7 +307,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return undefined;
     };
 
-    getAnchor = () => {
+    getAnchor = (addAsAnnotation: boolean) => {
         let ele: Opt<HTMLDivElement> = undefined;
         try {
             const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
@@ -317,7 +317,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             }
         } catch (e) {}
         const anchor =
-            this._getAnchor(this._savedAnnotations) ??
+            this._getAnchor(this._savedAnnotations, false) ??
             Docs.Create.WebanchorDocument(this._url, {
                 title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop),
                 y: NumCast(this.layoutDoc._scrollTop),
@@ -325,7 +325,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             });
         anchor.text = ele?.textContent ?? '';
         anchor.textHtml = ele?.innerHTML;
-        this.addDocumentWrapper(anchor);
+        addAsAnnotation && this.addDocumentWrapper(anchor);
         return anchor;
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 62e215521..deb1d68a7 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', false);
+    getAnchor = (addAsAnnotation: boolean) => this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
 
     @action
     setupAnchorMenu = () => {
@@ -239,23 +239,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
             !this.layoutDoc.showSidebar && this.toggleSidebar();
-            setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
+            setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
         };
         AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
             !this.layoutDoc.showSidebar && this.toggleSidebar();
-            const anchor = this.getAnchor();
-            const target = this._sidebarRef.current?.anchorMenuClick(anchor);
-            if (target) {
-                anchor.followLinkAudio = true;
-                DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
-                target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
-            }
+            const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true);
+            setTimeout(() => {
+                const target = this._sidebarRef.current?.anchorMenuClick(anchor);
+                if (target) {
+                    anchor.followLinkAudio = true;
+                    DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
+                    target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
+                }
+            });
         };
         AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
             this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
             return undefined;
         });
-        AnchorMenu.Instance.onMakeAnchor = this.getAnchor;
+        AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
         AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
         /**
          * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
@@ -270,7 +272,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 return target;
             };
 
-            DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), this.getAnchor, targetCreator), e.pageX, e.pageY);
+            DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
         });
         const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
         this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
@@ -518,7 +520,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
-        if (de.complete.annoDragData) de.complete.annoDragData.dropDocCreator = this.getAnchor;
+        if (de.complete.annoDragData) de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true);
         const dragData = de.complete.docDragData;
         if (dragData) {
             const draggedDoc = dragData.draggedDocuments.length && dragData.draggedDocuments[0];
@@ -689,9 +691,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {});
 
     @undoBatch
-    makePushpin = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle);
+    makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle);
 
-    isPushpin = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
+    isTargetToggler = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
 
     specificContextMenu = (e: React.MouseEvent): void => {
         const cm = ContextMenu.Instance;
@@ -714,8 +716,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc);
                         AnchorMenu.Instance.Pinned = false;
                         AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc);
-                        AnchorMenu.Instance.MakePushpin = () => this.makePushpin(anchor as Doc);
-                        AnchorMenu.Instance.IsPushpin = () => this.isPushpin(anchor as Doc);
+                        AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc);
+                        AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc);
                         AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
                     })
                 );
@@ -891,7 +893,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     // TODO: nda -- Look at how link anchors are added
-    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean) {
+    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean, addAsAnnotation?: boolean) {
         const state = this._editorView?.state;
         if (state) {
             let selectedText = '';
@@ -901,7 +903,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             if (sel.from !== sel.to) {
                 const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
                 const href = targetHref ?? Doc.localServerPath(anchor);
-                if (anchor !== anchorDoc) this.addDocument(anchor);
+                if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor);
                 tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
                     if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                         const allAnchors = [{ href, title, anchorId: anchor[Id] }];
@@ -1476,7 +1478,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             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 (target) return;
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index e3c67ad2e..5675776fb 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -259,7 +259,7 @@ export class RichTextRules {
                                 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: docid, _width: 500, _height: 500 }, docid);
-                            DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined);
+                            DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
 
                             const fstate = this.TextBox.EditorView?.state;
                             if (fstate && selection) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 855a7f171..226fd8d7d 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -104,7 +104,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return NumCast(this.rootDoc._itemIndex);
     }
     @computed get activeItem() {
-        return Cast(this.childDocs[NumCast(this.rootDoc._itemIndex)], Doc, null);
+        return DocCast(this.childDocs[NumCast(this.rootDoc._itemIndex)]);
     }
     @computed get targetDoc() {
         return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
@@ -452,7 +452,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presWidth = NumCast(targetDoc.width);
             pinDoc.presHeight = NumCast(targetDoc.height);
         }
-        if (pinProps.pinAudioPlay) pinDoc.followLinkAudio = true;
+        if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true;
         if (pinProps.pinData) {
             pinDoc.presPinData =
                 pinProps.pinData.scrollable ||
@@ -577,7 +577,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     noSelect: true,
                     originatingDoc: activeItem,
                     easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
-                    zoomTextSelections: true,
+                    zoomTextSelections: BoolCast(activeItem.presZoomText),
+                    playAudio: BoolCast(activeItem.presPlayAudio),
                 };
                 if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
                 var containerDocContext = srcContext ? [srcContext] : [];
@@ -1416,7 +1417,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             Effects
                             <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                                 <div className="presBox-subheading">Play Audio Annotation</div>
-                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.followLinkAudio = !BoolCast(activeItem.followLinkAudio))} checked={BoolCast(activeItem.followLinkAudio)} />
+                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} checked={BoolCast(activeItem.presPlayAudio)} />
+                            </div>
+                            <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+                                <div className="presBox-subheading">Zoom Text Selections</div>
+                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presZoomText = !BoolCast(activeItem.presZoomText))} checked={BoolCast(activeItem.presZoomText)} />
                             </div>
                             <div
                                 className="presBox-dropdown"
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 265328036..2fb795b06 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 { truncate } from 'lodash';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -52,13 +53,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
-    public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
-    public GetAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+    public Highlight: (color: string, isTargetToggler: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined;
+    public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined;
     public Delete: () => void = unimplementedFunction;
     public AddTag: (key: string, value: string) => boolean = returnFalse;
     public PinToPres: () => void = unimplementedFunction;
-    public MakePushpin: () => void = unimplementedFunction;
-    public IsPushpin: () => boolean = returnFalse;
+    public MakeTargetToggle: () => void = unimplementedFunction;
+    public IsTargetToggler: () => boolean = returnFalse;
     public get Active() {
         return this._left > 0;
     }
@@ -238,8 +239,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                               <FontAwesomeIcon icon="map-pin" size="lg" />
                           </button>
                       </Tooltip>,
-                      <Tooltip key="pushpin" title={<div className="dash-tooltip">{'toggle pushpin behavior'}</div>}>
-                          <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? 'black' : 'white', backgroundColor: this.IsPushpin() ? 'white' : 'black' }} onPointerDown={this.MakePushpin}>
+                      <Tooltip key="toggle" title={<div className="dash-tooltip">{'make target visibility toggle on click'}</div>}>
+                          <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}>
                               <FontAwesomeIcon icon="thumbtack" size="lg" />
                           </button>
                       </Tooltip>,
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 7069ff399..d8e44ae9d 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -55,9 +55,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     pinToPres = () => this.props.pinToPres(this.annoTextRegion, {});
 
     @undoBatch
-    makePushpin = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
+    makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
 
-    isPushpin = () => BoolCast(this.annoTextRegion.followLinkToggle);
+    isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle);
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
@@ -67,8 +67,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
             AnchorMenu.Instance.Pinned = false;
             AnchorMenu.Instance.AddTag = this.addTag.bind(this);
             AnchorMenu.Instance.PinToPres = this.pinToPres;
-            AnchorMenu.Instance.MakePushpin = this.makePushpin;
-            AnchorMenu.Instance.IsPushpin = this.isPushpin;
+            AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle;
+            AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler;
             AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
             e.stopPropagation();
         } else if (e.button === 0) {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index f95d5ac2e..b0b7816b8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -135,7 +135,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     copy = (e: ClipboardEvent) => {
         if (this.props.isContentActive() && e.clipboardData) {
             e.clipboardData.setData('text/plain', this._selectionText);
-            const anchor = this._getAnchor();
+            const anchor = this._getAnchor(undefined, false);
             if (anchor) {
                 anchor.textCopied = true;
                 e.clipboardData.setData('dash/pdfAnchor', anchor[Id]);
@@ -317,7 +317,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             this._ignoreScroll = false;
             if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio
             this._scrollTimer = setTimeout(() => {
-                DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!()!, false);
+                DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!(true)!, false);
                 this._scrollTimer = undefined;
             }, 200);
         }
-- 
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/nodes/MapBox/MapBox.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 32f5040c44dc302e3dd53cecd9be4cd51a474d3f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 14 Feb 2023 14:57:25 -0500
Subject: fixed pinning regions on pdf/web pages to allow proper pres
 following.  added click on pres item# to select w/o navigation.  fixed
 dashDocView to be selectable without losing selection to parent textbox. 
 added BingMaps rudimentary option to googleMaps

---
 package-lock.json                                  |  18 ++-
 package.json                                       |   2 +
 src/client/views/nodes/MapBox/MapBox.tsx           | 148 +++++++++++++--------
 src/client/views/nodes/PDFBox.tsx                  |   1 +
 src/client/views/nodes/WebBox.tsx                  |   1 +
 .../views/nodes/formattedText/DashDocView.tsx      |   5 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   3 +
 src/client/views/nodes/trails/PresBox.tsx          |  20 +--
 src/client/views/nodes/trails/PresElementBox.scss  |   6 +
 src/client/views/nodes/trails/PresElementBox.tsx   |  12 +-
 10 files changed, 143 insertions(+), 73 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 547a6e1d7..95a128419 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3675,6 +3675,11 @@
                 "file-uri-to-path": "1.0.0"
             }
         },
+        "bingmaps-react": {
+            "version": "1.2.10",
+            "resolved": "https://registry.npmjs.org/bingmaps-react/-/bingmaps-react-1.2.10.tgz",
+            "integrity": "sha512-fM887Sr6OIlo6ThmSpGfNEDssSytdnsMrbDTDs+YH/43etf0dlfcR4oaJvygy+fwn21hpP2lVvwTHUDZvGxZqA=="
+        },
         "bl": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -4970,9 +4975,9 @@
             }
         },
         "core-js": {
-            "version": "1.2.7",
-            "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
-            "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
+            "version": "3.28.0",
+            "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.28.0.tgz",
+            "integrity": "sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw=="
         },
         "core-js-pure": {
             "version": "3.22.8",
@@ -8260,6 +8265,13 @@
                 "promise": "^7.1.1",
                 "setimmediate": "^1.0.5",
                 "ua-parser-js": "^0.7.30"
+            },
+            "dependencies": {
+                "core-js": {
+                    "version": "1.2.7",
+                    "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
+                    "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA=="
+                }
             }
         },
         "fd-slicer": {
diff --git a/package.json b/package.json
index 712354355..00ce356f9 100644
--- a/package.json
+++ b/package.json
@@ -165,6 +165,7 @@
         "bcrypt-nodejs": "0.0.3",
         "bezier-curve": "^1.0.0",
         "bezier-js": "^4.1.1",
+        "bingmaps-react": "^1.2.10",
         "bluebird": "^3.7.2",
         "body-parser": "^1.19.2",
         "bootstrap": "^4.6.1",
@@ -181,6 +182,7 @@
         "connect-mongo": "^2.0.3",
         "cookie-parser": "^1.4.6",
         "cookie-session": "^2.0.0",
+        "core-js": "^3.28.0",
         "cors": "^2.8.5",
         "depcheck": "^0.9.2",
         "equation-editor-react": "github:bobzel/equation-editor-react#useLocally",
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 5940fc075..039f11cd1 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,5 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
+import BingMapsReact from 'bingmaps-react';
 import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -43,20 +44,22 @@ const mapContainerStyle = {
 };
 
 const defaultCenter = {
-    lat: 38.685,
-    lng: -115.234,
+    lat: 42.360081,
+    lng: -71.058884,
 };
 
 const mapOptions = {
     fullscreenControl: false,
 };
 
+const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/  and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
 const apiKey = process.env.GOOGLE_MAPS;
 
 const script = document.createElement('script');
 script.defer = true;
 script.async = true;
 script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+console.log(script.src);
 document.head.appendChild(script);
 
 /**
@@ -84,6 +87,7 @@ const options = {
 
 @observer
 export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() {
+    static UseBing = true;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -551,69 +555,99 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     infoHeight = () => this.props.PanelHeight() / 5;
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
     savedAnnotations = () => this._savedAnnotations;
+
+    _bingSearchManager: any;
+    _bingMap: any;
+    get MicrosoftMaps() {
+        return (window as any).Microsoft.Maps;
+    }
+    // uses Bing Search to retrieve lat/lng for a location.  eg.,
+    //   const results = this.geocodeQuery(map.map, 'Philadelphia, PA');
+    // to move the map to that location:
+    //     const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA');
+    //     this._bingMap.current.setView({
+    //        mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
+    //        center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
+    //     });
+    //
+    bingGeocode = (map: any, query: string) => {
+        return new Promise<{ latitude: number; longitude: number }>((res, reject) => {
+            //If search manager is not defined, load the search module.
+            if (!this._bingSearchManager) {
+                //Create an instance of the search manager and call the geocodeQuery function again.
+                this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => {
+                    this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current);
+                    res(this.bingGeocode(map, query));
+                });
+            } else {
+                this._bingSearchManager.geocode({
+                    where: query,
+                    callback: action((r: any) => {
+                        res(r.results[0].location);
+                    }),
+                    errorCallback: (e: any) => reject(),
+                });
+            }
+        });
+    };
+
+    bingViewOptions = {
+        center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng },
+        mapTypeId: 'grayscale',
+    };
+    bingMapOptions = {
+        navigationBarMode: 'square',
+    };
+    bingMapReady = (map: any) => (this._bingMap = map.map);
     render() {
         const renderAnnotations = (docFilters?: () => string[]) => null;
-        // bcz: commmented this out.  Otherwise, any documents that are rendered with an InfoWindow of a marker
-        //      will also be rendered as freeform annotations on the map. However, it doesn't seem that rendering
-        //      freeform documents on the map does anything anyway, so getting rid of it for now.  Also, since documents
-        //      are rendered twice, adding a new note to the InfoWindow loses focus immediately on creation since it gets
-        //      shifted to the non-visible view of the document in this freeform view.
-        // <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
-        //     renderDepth={this.props.renderDepth + 1}
-        //     isAnnotationOverlay={true}
-        //     fieldKey={this.annotationKey}
-        //     CollectionView={undefined}
-        //     setPreviewCursor={this.setPreviewCursor}
-        //     PanelWidth={this.panelWidth}
-        //     PanelHeight={this.panelHeight}
-        //     ScreenToLocalTransform={this.scrollXf}
-        //     scaling={returnOne}
-        //     dropAction={"alias"}
-        //     docFilters={docFilters || this.props.docFilters}
-        //     dontRenderDocuments={docFilters ? false : true}
-        //     select={emptyFunction}
-        //     bringToFront={emptyFunction}
-        //     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-        //     removeDocument={this.removeDocument}
-        //     moveDocument={this.moveDocument}
-        //     addDocument={this.sidebarAddDocument}
-        //     childPointerEvents={"all"}
-        //     pointerEvents={Doc.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
         return (
             <div className="mapBox" ref={this._ref}>
-                {/*console.log(apiKey)*/}
-                {/* <LoadScript
-                googleMapsApiKey={apiKey!}
-                libraries={['places', 'drawing']}
-            > */}
-                <div className="mapBox-wrapper" onWheel={e => e.stopPropagation()} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+                <div
+                    className="mapBox-wrapper"
+                    onWheel={e => e.stopPropagation()}
+                    onPointerDown={async e => {
+                        e.button === 0 && !e.ctrlKey && e.stopPropagation();
+                        // just a simple test of bing maps geocode api
+                        // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA');
+                        // this._bingMap.current.setView({
+                        //     mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
+                        //     center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
+                        //     zoom: 15,
+                        // });
+                    }}
+                    style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
                     <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
                     {renderAnnotations(this.opaqueFilter)}
                     {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
                     {this.annotationLayer}
-                    <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
-                        <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
-                            <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
-                        </Autocomplete>
-
-                        {this.renderMarkers()}
-                        {this.allMapMarkers
-                            .filter(marker => marker.infoWindowOpen)
-                            .map(marker => (
-                                <MapBoxInfoWindow
-                                    key={marker[Id]}
-                                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                                    place={marker}
-                                    markerMap={this.markerMap}
-                                    PanelWidth={this.infoWidth}
-                                    PanelHeight={this.infoHeight}
-                                    moveDocument={this.moveDocument}
-                                    isAnyChildContentActive={this.isAnyChildContentActive}
-                                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                                />
-                            ))}
-                        {/* {this.handleMapCenter(this._map)} */}
-                    </GoogleMap>
+
+                    {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />}
+                    <div style={{ display: MapBox.UseBing ? 'none' : undefined }}>
+                        <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
+                            <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
+                                <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
+                            </Autocomplete>
+
+                            {this.renderMarkers()}
+                            {this.allMapMarkers
+                                .filter(marker => marker.infoWindowOpen)
+                                .map(marker => (
+                                    <MapBoxInfoWindow
+                                        key={marker[Id]}
+                                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                                        place={marker}
+                                        markerMap={this.markerMap}
+                                        PanelWidth={this.infoWidth}
+                                        PanelHeight={this.infoHeight}
+                                        moveDocument={this.moveDocument}
+                                        isAnyChildContentActive={this.isAnyChildContentActive}
+                                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                                    />
+                                ))}
+                            {/* {this.handleMapCenter(this._map)} */}
+                        </GoogleMap>
+                    </div>
                     {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
                         <MarqueeAnnotator
                             rootDoc={this.rootDoc}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b88ac113e..5890c536d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -222,6 +222,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             const anchor = Docs.Create.TextanchorDocument({
                 title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
                 unrendered: true,
+                annotationOn: this.rootDoc,
             });
             PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc);
             return anchor;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c4cca9679..c392f3577 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -322,6 +322,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop),
                 y: NumCast(this.layoutDoc._scrollTop),
                 unrendered: true,
+                annotationOn: this.rootDoc,
             });
         anchor.text = ele?.textContent ?? '';
         anchor.textHtml = ele?.innerHTML;
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 722d0cc4f..19ee1fc7b 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -12,6 +12,7 @@ import { Transform } from '../../../util/Transform';
 import { DocumentView } from '../DocumentView';
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require('react');
+import { SelectionManager } from '../../../util/SelectionManager';
 
 export class DashDocView {
     dom: HTMLSpanElement; // container for label and value
@@ -192,10 +193,10 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
                     Document={this._finalLayout}
                     DataDoc={this._resolvedDataDoc}
                     addDocument={returnFalse}
-                    rootSelected={this._textBox.props.isSelected}
+                    rootSelected={returnFalse} //{this._textBox.props.isSelected}
                     removeDocument={this.removeDoc}
                     isDocumentActive={returnFalse}
-                    isContentActive={this._textBox.props.isContentActive}
+                    isContentActive={emptyFunction}
                     styleProvider={this._textBox.props.styleProvider}
                     docViewPath={this._textBox.props.docViewPath}
                     ScreenToLocalTransform={this.getDocTransform}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 80b18b8b9..49acd8a57 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1487,6 +1487,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             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) {
+                return;
+            }
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0a96297b7..e07517113 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -14,7 +14,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty
 import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs } from '../../../documents/Documents';
+import { Docs, DocumentOptions } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -869,13 +869,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         presDocView && SelectionManager.SelectView(presDocView, false);
     };
 
+    focusElement = (doc: Doc, options: DocFocusOptions) => this.selectElement(doc);
+
     //Regular click
     @action
-    selectElement = async (doc: Doc) => {
+    selectElement = async (doc: Doc, noNav = false) => {
         CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
-        this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
-        // if (doc.presPinView) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0);
-        // else
+        !noNav && this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
         this.updateCurrentPresentation(DocCast(doc.context));
     };
 
@@ -911,19 +911,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     //regular click
     @action
-    regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true) => {
+    regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true, noNav = false) => {
         this.clearSelectedArray();
         this.addToSelectedArray(doc);
         this._eleArray.splice(0, this._eleArray.length, ref);
         this._dragArray.splice(0, this._dragArray.length, drag);
-        focus && this.selectElement(doc);
+        focus && this.selectElement(doc, noNav);
         selectPres && this.selectPres();
     };
 
-    modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean) => {
+    modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean, noNav: boolean = false) => {
         if (cmdClick) this.multiSelect(doc, ref, drag);
         else if (shiftClick) this.shiftSelect(doc, ref, drag);
-        else this.regularSelect(doc, ref, drag, focus);
+        else this.regularSelect(doc, ref, drag, focus, noNav);
     };
 
     static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance?.keyEvents(e);
@@ -2253,7 +2253,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 filterAddDocument={this.addDocumentFilter}
                                 removeDocument={returnFalse}
                                 dontRegisterView={true}
-                                focus={this.selectElement}
+                                focus={this.focusElement}
                                 scriptContext={this}
                                 ScreenToLocalTransform={this.getTransform}
                                 AddToMap={this.AddToMap}
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 415253af1..4f95f0c1f 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -44,6 +44,12 @@ $slide-active: #5b9fdd;
     display: flex;
     align-items: center;
 
+    .presItem-number {
+        cursor: pointer;
+        &:hover {
+            background-color: $light-blue;
+        }
+    }
     .presItem-name {
         display: flex;
         min-width: 20px;
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 788900b46..f265c1315 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -592,7 +592,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
                                 cursor: isSelected ? 'text' : 'grab',
                             }}>
-                            <div>{`${this.indexInPres + 1}.  `}</div>
+                            <div
+                                className="presItem-number"
+                                title="select without navigation"
+                                style={{ pointerEvents: this.presBoxView?.isContentActive() ? 'all' : undefined }}
+                                onPointerDown={e => {
+                                    e.stopPropagation();
+                                    if (this._itemRef.current && this._dragRef.current) {
+                                        this.presBoxView?.modifierSelect(activeItem, this._itemRef.current, this._dragRef.current, false, false, false, true);
+                                    }
+                                }}
+                                onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}.  `}</div>
                             <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
                         </div>
                         {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
-- 
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/nodes/MapBox/MapBox.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 0fb643d13dc7dbdcd4ae0f4785f8f2bd3fbbc764 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 10 Mar 2023 21:48:11 -0500
Subject: fixed image anchors to save zoom/pan info and to focus() on content
 when following anchors.  fixed progresivized items in trails to not allow
 dragging items out or dropping new items in.

---
 src/client/documents/Documents.ts         |  3 ++
 src/client/views/collections/TreeView.tsx | 81 +++++++++++++++++--------------
 src/client/views/nodes/ImageBox.tsx       | 17 ++++++-
 src/client/views/nodes/MapBox/MapBox.tsx  |  7 +--
 src/client/views/nodes/trails/PresBox.tsx |  2 +-
 5 files changed, 68 insertions(+), 42 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6296b0df4..4dc1c3b71 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -261,6 +261,9 @@ export class DocumentOptions {
     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
+    presPanX?: number; // panX saved as a view spec
+    presPanY?: number; // panY saved as a view spec
+    presViewScale?: number; // viewScale saved as a view Spec
     presTransition?: number; //the time taken for the transition TO a document
     presDuration?: number; //the duration of the slide in presentation view
     presZoomText?: boolean; // whether text anchors should shown in a larger box when following links to make them stand out
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index c8dc68eae..054d61d0c 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -8,7 +8,7 @@ import { List } from '../../../fields/List';
 import { RichTextField } from '../../../fields/RichTextField';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -169,23 +169,6 @@ export class TreeView extends React.Component<TreeViewProps> {
     @computed get selected() {
         return SelectionManager.IsSelected(this._docRef);
     }
-    //      SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
-
-    @observable _presTimer!: NodeJS.Timeout;
-    @observable _presKeyEventsActive: boolean = false;
-
-    @observable _selectedArray: ObservableMap = new ObservableMap<Doc, any>();
-    // the selected item's index
-    @computed get itemIndex() {
-        return NumCast(this.doc._itemIndex);
-    }
-    // the item that's active
-    @computed get activeItem() {
-        return this.childDocs ? Cast(this.childDocs[NumCast(this.doc._itemIndex)], Doc, null) : undefined;
-    }
-    @computed get targetDoc() {
-        return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
-    }
 
     childDocList(field: string) {
         const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
@@ -195,11 +178,17 @@ export class TreeView extends React.Component<TreeViewProps> {
             DocListCastOrNull(this.doc[field])
         ); // otherwise use the document's data field
     }
+    moving: boolean = false;
     @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
         if (this.doc !== target && addDoc !== returnFalse) {
+            const canAdd1 = (this.props.parentTreeView as any).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this.props.parentTreeView?.doc.data)) instanceof ComputedField);
+
             // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
-            if (this.props.removeDoc?.(doc) === true) {
-                return addDoc(doc);
+            if (canAdd1 && this.props.removeDoc?.(doc) === true) {
+                this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = true);
+                const res = addDoc(doc);
+                this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = false);
+                return res;
             }
         }
         return false;
@@ -207,6 +196,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     @undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
         this.props.treeView.props.select(false);
         const ind = this.dataDoc[key].indexOf(doc);
+
         const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
         res && ind > 0 && DocumentManager.Instance.getDocumentView(this.dataDoc[key][ind - 1], this.props.treeView.props.CollectionView)?.select(false);
         return res;
@@ -378,20 +368,32 @@ export class TreeView extends React.Component<TreeViewProps> {
         const docDragData = de.complete.docDragData;
         if (docDragData && pt[0] < rect.left + rect.width) {
             if (docDragData.draggedDocuments[0] === this.doc) return true;
-            if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
+            if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.removeDocument, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
                 e.stopPropagation();
             }
         }
     };
 
-    dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
+    dropping: boolean = false;
+    dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, removeDocument: DragManager.RemoveFunction | undefined, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
         const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
-        const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes('add')) || forceAdd;
-        const localAdd = (doc: Doc) => (Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false);
-        const addDoc = !inside ? parentAddDoc : (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
+        const localAdd = (doc: Doc | Doc[]) => {
+            const innerAdd = (doc: Doc) => {
+                const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
+                const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
+                dataIsComputed && (doc.context = this.doc.context);
+                return added;
+            };
+            return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
+        };
+        const addDoc = inside ? localAdd : parentAddDoc;
         const move = (!dropAction || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same') && moveDocument;
+        const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes('add')) || forceAdd;
         if (canAdd) {
-            return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false));
+            this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true);
+            const res = UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false));
+            this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = false);
+            return res;
         }
         return false;
     }
@@ -437,12 +439,16 @@ export class TreeView extends React.Component<TreeViewProps> {
             const expandedWidth = () => this.props.panelWidth() - leftOffset.width;
             if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) {
                 const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
-                const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
-                    const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
-                    added && (doc.context = this.doc.context);
-                    return added;
+                const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
+                const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => {
+                    const innerAdd = (doc: Doc) => {
+                        const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
+                        const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
+                        dataIsComputed && (doc.context = this.doc.context);
+                        return added;
+                    };
+                    return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
                 };
-                const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
                 contentElement = TreeView.GetChildElements(
                     contents instanceof Doc ? [contents] : DocListCast(contents),
                     this.props.treeView,
@@ -453,7 +459,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                     this.props.prevSibling,
                     addDoc,
                     remDoc,
-                    this.move,
+                    moveDoc,
                     this.props.dropAction,
                     this.props.addDocTab,
                     this.titleStyleProvider,
@@ -533,6 +539,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             );
             const key = (expandKey === 'annotations' ? `${this.fieldKey}-` : '') + expandKey;
             const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
+            const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
             const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
                 // if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't),
                 // then the modification would be done here
@@ -543,8 +550,10 @@ export class TreeView extends React.Component<TreeViewProps> {
                     docs.push(doc);
                     docs.sort((a, b) => (NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1)).forEach((d, i) => (d.zIndex = i));
                 }
-                const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
-                added && (doc.context = this.doc.context);
+                const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
+                const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
+                !dataIsComputed && added && (doc.context = this.doc.context);
+
                 return added;
             };
             const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
@@ -593,7 +602,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                                   this.props.prevSibling,
                                   addDoc,
                                   remDoc,
-                                  this.move,
+                                  moveDoc,
                                   StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType,
                                   this.props.addDocTab,
                                   this.titleStyleProvider,
@@ -1064,7 +1073,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         const before = pt[1] < rect.top + rect.height / 2;
         const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
 
-        const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, false));
+        const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false));
     };
 
     render() {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c6d4cb694..e60a03190 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -27,7 +27,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
-import { OpenWhere } from './DocumentView';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
 import { FaceRectangles } from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
@@ -73,7 +73,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor =
             this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
-            Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+            Docs.Create.ImageanchorDocument({
+                title: 'ImgAnchor:' + this.rootDoc.title,
+                presPanX: NumCast(this.layoutDoc._panX),
+                presPanY: NumCast(this.layoutDoc._panY),
+                presViewScale: Cast(this.layoutDoc._viewScale, 'number', null),
+                presTransition: 1000,
+                unrendered: true,
+                annotationOn: this.rootDoc,
+            });
         if (anchor) {
             if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
             /* addAsAnnotation &&*/ this.addDocument(anchor);
@@ -433,6 +441,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this._marqueeing = undefined;
         this.props.select(false);
     };
+    focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options);
+
+    _ffref = React.createRef<CollectionFreeFormView>();
     savedAnnotations = () => this._savedAnnotations;
     render() {
         TraceMobx();
@@ -460,6 +471,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined,
                 }}>
                 <CollectionFreeFormView
+                    ref={this._ffref}
                     {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
                     renderDepth={this.props.renderDepth + 1}
                     fieldKey={this.annotationKey}
@@ -471,6 +483,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     PanelHeight={this.props.PanelHeight}
                     ScreenToLocalTransform={this.screenToLocalTransform}
                     select={emptyFunction}
+                    focus={this.focus}
                     getScrollHeight={this.getScrollHeight}
                     NativeDimScaling={returnOne}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c182dfd19..0064a2022 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -21,6 +21,7 @@ import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { Annotation } from '../../pdf/Annotation';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
 import './MapBox.scss';
 import { MapBoxInfoWindow } from './MapBoxInfoWindow';
 
@@ -133,8 +134,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     private _sidebarRef = React.createRef<SidebarAnnos>();
     private _ref: React.RefObject<HTMLDivElement> = React.createRef();
 
-    constructor(props: any) {
-        super(props);
+    componentDidMount() {
+        this.props.setContentView?.(this);
     }
 
     @action
@@ -525,7 +526,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         );
     }
 
-    getAnchor = (addAsAnnotation: boolean) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
 
     /**
      * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2d9aaeca4..dda96d8e7 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -557,7 +557,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 });
             setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
         }
-        if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !PresBox.targetRenderedDoc(activeItem)._isGroup) {
+        if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !bestTarget._isGroup) {
             const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
             if (contentBounds) {
                 const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
-- 
cgit v1.2.3-70-g09d2


From 6ea3d7bb225b4f05ff83eba9428e865690e24f8a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 27 Mar 2023 18:44:05 -0400
Subject: fixed pointer events when not selected for MapBox

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

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

diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 0064a2022..ea8f47b39 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -616,7 +616,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         //     zoom: 15,
                         // });
                     }}
-                    style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+                    style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
                     <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
                     {renderAnnotations(this.opaqueFilter)}
                     {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
-- 
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/nodes/MapBox/MapBox.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

