From ae324ff50865929be836edf3bbf129207638a9c9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Nov 2022 16:26:31 -0500
Subject: big changes to make link following use the same code as pinning docs
 for trails.

---
 src/client/views/nodes/PDFBox.tsx | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

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

diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fcb3ccb07..39e323247 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,7 +5,7 @@ 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 { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -27,6 +27,7 @@ import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
 import { VideoBox } from './VideoBox';
 import React = require('react');
+import { PresBox } from './trails';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -207,16 +208,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        return this._pdfViewer?.scrollFocus(doc, smooth) ?? (didToggle ? 1 : undefined);
+        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, NumCast(doc.presTransition, NumCast(doc.focusSpeed, 500)), { pannable: doc.presPinData ? true : false });
+        return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), smooth) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
-        const anchor =
-            this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ??
-            Docs.Create.TextanchorDocument({
+        const docAnchor = () => {
+            const anchor = Docs.Create.TextanchorDocument({
                 title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
-                y: NumCast(this.layoutDoc._scrollTop),
                 unrendered: true,
             });
+            PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc);
+            return anchor;
+        };
+        const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? docAnchor();
         this.addDocument(anchor);
         return anchor;
     };
-- 
cgit v1.2.3-70-g09d2


From 66184a172006de4d4bf72d9da33858e04d298181 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 1 Dec 2022 10:13:03 -0500
Subject: refactored process of following links / jumping to docs and added
 following options for zoomTime, etc instead of setting temporary fields on
 docs.

---
 src/client/documents/Documents.ts                  |   8 +-
 src/client/util/DocumentManager.ts                 |  98 +++---
 .../util/Import & Export/DirectoryImportBox.tsx    | 334 +++++++++++----------
 src/client/util/LinkFollower.ts                    |  84 +++---
 src/client/util/SharingManager.tsx                 |   2 +-
 src/client/views/DocComponent.tsx                  |   2 +-
 src/client/views/InkingStroke.tsx                  |   6 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/MarqueeAnnotator.tsx              |   2 +-
 src/client/views/PropertiesView.tsx                |  28 +-
 .../views/collections/CollectionNoteTakingView.tsx |   5 +-
 .../views/collections/CollectionStackingView.tsx   |   5 +-
 src/client/views/collections/TabDocView.tsx        |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   8 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 .../collectionLinear/CollectionLinearView.tsx      |   2 +-
 .../collectionSchema/CollectionSchemaCells.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  26 +-
 src/client/views/nodes/ImageBox.tsx                |   8 +-
 src/client/views/nodes/PDFBox.tsx                  |  13 +-
 src/client/views/nodes/VideoBox.tsx                |   6 +-
 src/client/views/nodes/WebBox.tsx                  |  10 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 src/client/views/nodes/trails/PresBox.tsx          |  24 +-
 src/client/views/pdf/Annotation.tsx                |   4 +-
 src/client/views/pdf/PDFViewer.tsx                 |   8 +-
 src/client/views/search/SearchBox.tsx              |   2 +-
 27 files changed, 354 insertions(+), 353 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index eed839520..d13d96dd3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -157,7 +157,7 @@ export class DocumentOptions {
     _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom]
     _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
     _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
-    _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target
+    _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)
     _showCaption?: string; // which field to display in the caption area.  leave empty to have no caption
     _scrollTop?: number; // scroll location for pdfs
@@ -272,7 +272,7 @@ export class DocumentOptions {
     clipWidth?: number; // percent transition from before to after in comparisonBox
     dockingConfig?: string;
     annotationOn?: Doc;
-    isPushpin?: boolean;
+    followLinkToggle?: boolean;
     isGroup?: boolean; // whether a collection should use a grouping UI behavior
     _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped.  e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
     noteType?: string;
@@ -1703,7 +1703,7 @@ export namespace DocUtils {
     }
 
     export function LeavePushpin(doc: Doc, annotationField: string) {
-        if (doc.isPushpin) return undefined;
+        if (doc.followLinkToggle) return undefined;
         const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
         const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
         if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
@@ -1711,7 +1711,7 @@ export namespace DocUtils {
                 title: 'pushpin',
                 label: '',
                 annotationOn: Cast(doc.annotationOn, Doc, null),
-                isPushpin: true,
+                followLinkToggle: true,
                 icon: 'map-pin',
                 x: Cast(doc.x, 'number', null),
                 y: Cast(doc.y, 'number', null),
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4f02a8202..1b63b615b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -5,7 +5,7 @@ import { Cast, DocCast } from '../../fields/Types';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
@@ -171,19 +171,13 @@ export class DocumentManager {
     };
     public jumpToDocument = (
         targetDoc: Doc, // document to display
-        willZoom: boolean, // whether to zoom doc to take up most of screen
+        options: DocFocusOptions, // options for how to navigate to target
         createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
         docContext: Doc[], // context to load that should contain the target
-        linkDoc?: Doc, // link that's being followed
-        closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there
-        originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
-        finished?: () => void,
-        originalTarget?: Doc,
-        noSelect?: boolean,
-        presZoomScale?: number
+        finished?: () => void
     ): void => {
-        originalTarget = originalTarget ?? targetDoc;
-        const docView = this.getFirstDocumentView(targetDoc, originatingDoc);
+        const originalTarget = options.originalTarget ?? targetDoc;
+        const docView = this.getFirstDocumentView(targetDoc, options.originatingDoc);
         const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
         const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
         var wasHidden = resolvedTarget.hidden;
@@ -195,14 +189,14 @@ export class DocumentManager {
         }
         const focusAndFinish = (didFocus: boolean) => {
             const finalTargetDoc = resolvedTarget;
-            if (originatingDoc?.isPushpin) {
+            if (options.toggleTarget) {
                 if (!didFocus && !wasHidden) {
                     // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
                     finalTargetDoc.hidden = !finalTargetDoc.hidden;
                 }
             } else {
                 finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
-                !noSelect && docView?.select(false);
+                !options.noSelect && docView?.select(false);
             }
             finished?.();
         };
@@ -216,9 +210,8 @@ export class DocumentManager {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
                 annoContainerView.iconify(() =>
                     annoContainerView.focus(targetDoc, {
+                        ...options,
                         originalTarget,
-                        willZoom,
-                        scale: presZoomScale,
                         afterFocus: (didFocus: boolean) =>
                             new Promise<ViewAdjustment>(res => {
                                 focusAndFinish(true);
@@ -232,13 +225,12 @@ export class DocumentManager {
             }
         }
         if (focusView) {
-            !noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
-            if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
+            !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
+            if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
-                focusView.focus(originalTarget ?? targetDoc, {
+                focusView.focus(originalTarget, {
+                    ...options,
                     originalTarget,
-                    willZoom,
-                    scale: presZoomScale,
                     afterFocus: (didFocus: boolean) =>
                         new Promise<ViewAdjustment>(res => {
                             focusAndFinish(forceDidFocus || didFocus);
@@ -262,13 +254,12 @@ export class DocumentManager {
                     targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
                     targetDocContext._viewTransition = 'transform 500ms';
                     targetDocContextView.props.focus(targetDocContextView.rootDoc, {
-                        willZoom,
+                        ...options,
+                        // originalTarget, // needed?
                         afterFocus: async () => {
                             targetDocContext._viewTransition = undefined;
                             if (targetDocContext.layoutKey === 'layout_icon') {
-                                targetDocContextView.iconify(() =>
-                                    this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
-                                );
+                                targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished));
                             }
                             return ViewAdjustment.doNothing;
                         },
@@ -281,56 +272,35 @@ export class DocumentManager {
                         finished?.();
                     } else {
                         // no timecode means we need to find the context view and focus on our target
-                        const findView = (delay: number) => {
-                            const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
-                            if (retryDocView) {
-                                // we found the target in the context.
-                                Doc.linkFollowHighlight(retryDocView.rootDoc);
-                                retryDocView.focus(targetDoc, {
-                                    willZoom,
-                                    afterFocus: (didFocus: boolean) =>
-                                        new Promise<ViewAdjustment>(res => {
-                                            !noSelect && focusAndFinish(true);
-                                            res(ViewAdjustment.doNothing);
-                                        }),
-                                }); // focus on the target in the context
-                            } else if (delay > 1000) {
-                                // we didn't find the target, so it must have moved out of the context.  Go back to just creating it.
-                                if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc);
-                                if (targetDoc.layout) {
-                                    // there will no layout for a TEXTANCHOR type document
-                                    createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
-                                }
-                            } else {
-                                setTimeout(() => findView(delay + 200), 200);
-                            }
-                        };
-                        setTimeout(() => findView(0), 0);
+                        const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
+                        if (retryDocView) {
+                            // we found the target in the context.
+                            Doc.linkFollowHighlight(retryDocView.rootDoc);
+                            retryDocView.focus(targetDoc, {
+                                ...options,
+                                // originalTarget -- needed?
+                                afterFocus: (didFocus: boolean) =>
+                                    new Promise<ViewAdjustment>(res => {
+                                        !options.noSelect && focusAndFinish(true);
+                                        res(ViewAdjustment.doNothing);
+                                    }),
+                            }); // focus on the target in the context
+                        } else if (targetDoc.layout) {
+                            // there will no layout for a TEXTANCHOR type document
+                            createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
+                        }
                     }
                 } else {
                     if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
                         const docContextView = this.getFirstDocumentView(docContext[0]);
                         if (docContextView) {
-                            return docContextView.iconify(() =>
-                                this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
-                            );
+                            return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished));
                         }
                     }
                     // there's no context view so we need to create one first and try again when that finishes
                     createViewFunc(
                         targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
-                        () =>
-                            this.jumpToDocument(
-                                targetDoc,
-                                willZoom,
-                                (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished),
-                                docContext,
-                                linkDoc,
-                                true /* if target not found, get rid of context just created */,
-                                originatingDoc,
-                                finished,
-                                originalTarget
-                            )
+                        () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished)
                     );
                 }
             }
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 37571ae01..916eee4b7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,27 +1,27 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { BatchedArray } from "array-batcher";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import Measure, { ContentRect } from "react-measure";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { SchemaHeaderField } from "../../../fields/SchemaHeaderField";
-import { BoolCast, Cast, NumCast } from "../../../fields/Types";
-import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes";
-import { Utils } from "../../../Utils";
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { Networking } from "../../Network";
-import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
-import { DocumentManager } from "../DocumentManager";
-import "./DirectoryImportBox.scss";
-import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
-import React = require("react");
+import { BatchedArray } from 'array-batcher';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import Measure, { ContentRect } from 'react-measure';
+import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { listSpec } from '../../../fields/Schema';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { BoolCast, Cast, NumCast } from '../../../fields/Types';
+import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes';
+import { Utils } from '../../../Utils';
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
+import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
+import { Networking } from '../../Network';
+import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
+import { DocumentManager } from '../DocumentManager';
+import './DirectoryImportBox.scss';
+import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry';
+import React = require('react');
 
-const unsupported = ["text/html", "text/plain"];
+const unsupported = ['text/html', 'text/plain'];
 
 @observer
 export class DirectoryImportBox extends React.Component<FieldViewProps> {
@@ -29,7 +29,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
     @observable private top = 0;
     @observable private left = 0;
     private dimensions = 50;
-    @observable private phase = "";
+    @observable private phase = '';
     private disposer: Opt<IReactionDisposer>;
 
     @observable private entries: ImportMetadataEntry[] = [];
@@ -40,7 +40,9 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
     @observable private uploading = false;
     @observable private removeHover = false;
 
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(DirectoryImportBox, fieldKey);
+    }
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -71,7 +73,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
     handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
         runInAction(() => {
             this.uploading = true;
-            this.phase = "Initializing download...";
+            this.phase = 'Initializing download...';
         });
 
         const docs: Doc[] = [];
@@ -79,7 +81,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const files = e.target.files;
         if (!files || files.length === 0) return;
 
-        const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
+        const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0];
 
         const validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
@@ -100,7 +102,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const sizes: number[] = [];
         const modifiedDates: number[] = [];
 
-        runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
+        runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`));
 
         const batched = BatchedArray.from(validated, { batchSize: 15 });
         const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => {
@@ -109,23 +111,28 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
                 modifiedDates.push(file.lastModified);
             });
             collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
-            runInAction(() => this.completed += batch.length);
+            runInAction(() => (this.completed += batch.length));
         });
 
-        await Promise.all(uploads.map(async response => {
-            const { source: { type }, result } = response;
-            if (result instanceof Error) {
-                return;
-            }
-            const { accessPaths, exifData } = result;
-            const path = Utils.prepend(accessPaths.agnostic.client);
-            const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 });
-            const { data, error } = exifData;
-            if (document) {
-                Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
-                docs.push(document);
-            }
-        }));
+        await Promise.all(
+            uploads.map(async response => {
+                const {
+                    source: { type },
+                    result,
+                } = response;
+                if (result instanceof Error) {
+                    return;
+                }
+                const { accessPaths, exifData } = result;
+                const path = Utils.prepend(accessPaths.agnostic.client);
+                const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 }));
+                const { data, error } = exifData;
+                if (document) {
+                    Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
+                    docs.push(document);
+                }
+            })
+        );
 
         for (let i = 0; i < docs.length; i++) {
             const doc = docs[i];
@@ -146,7 +153,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             _height: 500,
             _chromeHidden: true,
             x: NumCast(doc.x),
-            y: NumCast(doc.y) + offset
+            y: NumCast(doc.y) + offset,
         };
         const parent = this.props.ContainingCollectionView;
         if (parent) {
@@ -154,14 +161,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             if (docs.length < 50) {
                 importContainer = Docs.Create.MasonryDocument(docs, options);
             } else {
-                const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")];
+                const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')];
                 importContainer = Docs.Create.SchemaDocument(headers, docs, options);
             }
-            runInAction(() => this.phase = 'External: uploading files to Google Photos...');
+            runInAction(() => (this.phase = 'External: uploading files to Google Photos...'));
             await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
-            Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+            Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer);
             !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
-            DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []);
+            DocumentManager.Instance.jumpToDocument(importContainer, { willZoom: true }, undefined, []);
         }
 
         runInAction(() => {
@@ -169,14 +176,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             this.quota = 1;
             this.completed = 0;
         });
-    }
+    };
 
     componentDidMount() {
-        this.selector.current!.setAttribute("directory", "");
-        this.selector.current!.setAttribute("webkitdirectory", "");
+        this.selector.current!.setAttribute('directory', '');
+        this.selector.current!.setAttribute('webkitdirectory', '');
         this.disposer = reaction(
             () => this.completed,
-            completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)
+            completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`))
         );
     }
 
@@ -193,7 +200,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const offset = this.dimensions / 2;
         this.left = bounds.width / 2 - offset;
         this.top = bounds.height / 2 - offset;
-    }
+    };
 
     @action
     addMetadataEntry = async () => {
@@ -201,8 +208,8 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         entryDoc.checked = false;
         entryDoc.key = keyPlaceholder;
         entryDoc.value = valuePlaceholder;
-        Doc.AddDocToList(this.props.Document, "data", entryDoc);
-    }
+        Doc.AddDocToList(this.props.Document, 'data', entryDoc);
+    };
 
     @action
     remove = async (entry: ImportMetadataEntry) => {
@@ -217,7 +224,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
                 }
             }
         }
-    }
+    };
 
     render() {
         const dimensions = 50;
@@ -228,193 +235,204 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const uploading = this.uploading;
         const showRemoveLabel = this.removeHover;
         const persistent = this.persistent;
-        let percent = `${completed / quota * 100}`;
-        percent = percent.split(".")[0];
-        percent = percent.startsWith("100") ? "99" : percent;
+        let percent = `${(completed / quota) * 100}`;
+        percent = percent.split('.')[0];
+        percent = percent.startsWith('100') ? '99' : percent;
         const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
-        const message = <span className={"phase"}>{this.phase}</span>;
-        const centerPiece = this.phase.includes("Google Photos") ?
-            <img src={"/assets/google_photos.png"} style={{
-                transition: "0.4s opacity ease",
-                width: 30,
-                height: 30,
-                opacity: uploading ? 1 : 0,
-                pointerEvents: "none",
-                position: "absolute",
-                left: 12,
-                top: this.top + 10,
-                fontSize: 18,
-                color: "white",
-                marginLeft: this.left + marginOffset
-            }} />
-            : <div
+        const message = <span className={'phase'}>{this.phase}</span>;
+        const centerPiece = this.phase.includes('Google Photos') ? (
+            <img
+                src={'/assets/google_photos.png'}
                 style={{
-                    transition: "0.4s opacity ease",
+                    transition: '0.4s opacity ease',
+                    width: 30,
+                    height: 30,
                     opacity: uploading ? 1 : 0,
-                    pointerEvents: "none",
-                    position: "absolute",
+                    pointerEvents: 'none',
+                    position: 'absolute',
+                    left: 12,
+                    top: this.top + 10,
+                    fontSize: 18,
+                    color: 'white',
+                    marginLeft: this.left + marginOffset,
+                }}
+            />
+        ) : (
+            <div
+                style={{
+                    transition: '0.4s opacity ease',
+                    opacity: uploading ? 1 : 0,
+                    pointerEvents: 'none',
+                    position: 'absolute',
                     left: 10,
                     top: this.top + 12.3,
                     fontSize: 18,
-                    color: "white",
-                    marginLeft: this.left + marginOffset
-                }}>{percent}%</div>;
+                    color: 'white',
+                    marginLeft: this.left + marginOffset,
+                }}>
+                {percent}%
+            </div>
+        );
         return (
             <Measure offset onResize={this.preserveCentering}>
-                {({ measureRef }) =>
-                    <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+                {({ measureRef }) => (
+                    <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}>
                         {message}
                         <input
-                            id={"selector"}
+                            id={'selector'}
                             ref={this.selector}
                             onChange={this.handleSelection}
                             type="file"
                             style={{
-                                position: "absolute",
-                                display: "none"
-                            }} />
+                                position: 'absolute',
+                                display: 'none',
+                            }}
+                        />
                         <label
-                            htmlFor={"selector"}
+                            htmlFor={'selector'}
                             style={{
                                 opacity: isEditing ? 0 : 1,
-                                pointerEvents: isEditing ? "none" : "all",
-                                transition: "0.4s ease opacity"
-                            }}
-                        >
-                            <div style={{
-                                width: dimensions,
-                                height: dimensions,
-                                borderRadius: "50%",
-                                background: "black",
-                                position: "absolute",
-                                left: this.left,
-                                top: this.top
-                            }} />
-                            <div style={{
-                                position: "absolute",
-                                left: this.left + 8,
-                                top: this.top + 10,
-                                opacity: uploading ? 0 : 1,
-                                transition: "0.4s opacity ease"
+                                pointerEvents: isEditing ? 'none' : 'all',
+                                transition: '0.4s ease opacity',
                             }}>
-                                <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} />
+                            <div
+                                style={{
+                                    width: dimensions,
+                                    height: dimensions,
+                                    borderRadius: '50%',
+                                    background: 'black',
+                                    position: 'absolute',
+                                    left: this.left,
+                                    top: this.top,
+                                }}
+                            />
+                            <div
+                                style={{
+                                    position: 'absolute',
+                                    left: this.left + 8,
+                                    top: this.top + 10,
+                                    opacity: uploading ? 0 : 1,
+                                    transition: '0.4s opacity ease',
+                                }}>
+                                <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} />
                             </div>
                             <img
                                 style={{
                                     width: 80,
                                     height: 80,
-                                    transition: "0.4s opacity ease",
+                                    transition: '0.4s opacity ease',
                                     opacity: uploading ? 0.7 : 0,
-                                    position: "absolute",
+                                    position: 'absolute',
                                     top: this.top - 15,
-                                    left: this.left - 15
+                                    left: this.left - 15,
                                 }}
-                                src={"/assets/loading.gif"}></img>
+                                src={'/assets/loading.gif'}></img>
                         </label>
                         <input
-                            type={"checkbox"}
-                            onChange={e => runInAction(() => this.persistent = e.target.checked)}
+                            type={'checkbox'}
+                            onChange={e => runInAction(() => (this.persistent = e.target.checked))}
                             style={{
                                 margin: 0,
-                                position: "absolute",
+                                position: 'absolute',
                                 left: 10,
                                 bottom: 10,
                                 opacity: isEditing || uploading ? 0 : 1,
-                                transition: "0.4s opacity ease",
-                                pointerEvents: isEditing || uploading ? "none" : "all"
+                                transition: '0.4s opacity ease',
+                                pointerEvents: isEditing || uploading ? 'none' : 'all',
                             }}
                             checked={this.persistent}
-                            onPointerEnter={action(() => this.removeHover = true)}
-                            onPointerLeave={action(() => this.removeHover = false)}
+                            onPointerEnter={action(() => (this.removeHover = true))}
+                            onPointerLeave={action(() => (this.removeHover = false))}
                         />
                         <p
                             style={{
-                                position: "absolute",
+                                position: 'absolute',
                                 left: 27,
                                 bottom: 8.4,
                                 fontSize: 12,
                                 opacity: showRemoveLabel ? 1 : 0,
-                                transition: "0.4s opacity ease"
-                            }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
+                                transition: '0.4s opacity ease',
+                            }}>
+                            Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload
+                        </p>
                         {centerPiece}
                         <div
                             style={{
-                                position: "absolute",
+                                position: 'absolute',
                                 top: 10,
                                 right: 10,
-                                borderRadius: "50%",
+                                borderRadius: '50%',
                                 width: 25,
                                 height: 25,
-                                background: "black",
-                                pointerEvents: uploading ? "none" : "all",
+                                background: 'black',
+                                pointerEvents: uploading ? 'none' : 'all',
                                 opacity: uploading ? 0 : 1,
-                                transition: "0.4s opacity ease"
+                                transition: '0.4s opacity ease',
                             }}
-                            title={isEditing ? "Back to Upload" : "Add Metadata"}
-                            onClick={action(() => this.editingMetadata = !this.editingMetadata)}
+                            title={isEditing ? 'Back to Upload' : 'Add Metadata'}
+                            onClick={action(() => (this.editingMetadata = !this.editingMetadata))}
                         />
                         <FontAwesomeIcon
                             style={{
-                                pointerEvents: "none",
-                                position: "absolute",
+                                pointerEvents: 'none',
+                                position: 'absolute',
                                 right: isEditing ? 14 : 15,
                                 top: isEditing ? 15.4 : 16,
                                 opacity: uploading ? 0 : 1,
-                                transition: "0.4s opacity ease"
+                                transition: '0.4s opacity ease',
                             }}
-                            icon={isEditing ? "cloud-upload-alt" : "tag"}
+                            icon={isEditing ? 'cloud-upload-alt' : 'tag'}
                             color="#FFFFFF"
-                            size={"1x"}
+                            size={'1x'}
                         />
                         <div
                             style={{
-                                transition: "0.4s ease opacity",
-                                width: "100%",
-                                height: "100%",
-                                pointerEvents: isEditing ? "all" : "none",
+                                transition: '0.4s ease opacity',
+                                width: '100%',
+                                height: '100%',
+                                pointerEvents: isEditing ? 'all' : 'none',
                                 opacity: isEditing ? 1 : 0,
-                                overflowY: "scroll"
-                            }}
-                        >
+                                overflowY: 'scroll',
+                            }}>
                             <div
                                 style={{
-                                    borderRadius: "50%",
+                                    borderRadius: '50%',
                                     width: 25,
                                     height: 25,
                                     marginLeft: 10,
-                                    position: "absolute",
+                                    position: 'absolute',
                                     right: 41,
-                                    top: 10
+                                    top: 10,
                                 }}
-                                title={"Add Metadata Entry"}
-                                onClick={this.addMetadataEntry}
-                            >
+                                title={'Add Metadata Entry'}
+                                onClick={this.addMetadataEntry}>
                                 <FontAwesomeIcon
                                     style={{
-                                        pointerEvents: "none",
+                                        pointerEvents: 'none',
                                         marginLeft: 6.4,
-                                        marginTop: 5.2
+                                        marginTop: 5.2,
                                     }}
-                                    icon={"plus"}
-                                    size={"1x"}
+                                    icon={'plus'}
+                                    size={'1x'}
                                 />
                             </div>
-                            <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
-                            <hr style={{ margin: "6px 10px 12px 10px" }} />
-                            {entries.map(doc =>
+                            <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p>
+                            <hr style={{ margin: '6px 10px 12px 10px' }} />
+                            {entries.map(doc => (
                                 <ImportMetadataEntry
                                     Document={doc}
                                     key={doc[Id]}
                                     remove={this.remove}
-                                    ref={(el) => { if (el) this.entries.push(el); }}
+                                    ref={el => {
+                                        if (el) this.entries.push(el);
+                                    }}
                                     next={this.addMetadataEntry}
                                 />
-                            )}
+                            ))}
                         </div>
                     </div>
-                }
+                )}
             </Measure>
         );
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 282116f1b..4f742817a 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,10 +1,11 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types';
+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 { DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
+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';
@@ -56,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, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
+                docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), 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
@@ -81,52 +82,45 @@ export class LinkFollower {
         const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0);
         const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
         const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
-        const followLinks = sourceDoc.isPushpin || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
+        const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
         var count = 0;
         const allFinished = () => ++count === followLinks.length && finished?.();
         followLinks.forEach(async linkDoc => {
-            if (linkDoc) {
-                const target = (
-                    sourceDoc === linkDoc.anchor1
-                        ? linkDoc.anchor2
-                        : sourceDoc === linkDoc.anchor2
-                        ? linkDoc.anchor1
-                        : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)
-                        ? linkDoc.anchor2
-                        : linkDoc.anchor1
-                ) as Doc;
-                if (target) {
-                    const zoom = BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false);
-                    if (target.TourMap) {
-                        const fieldKey = Doc.LayoutFieldKey(target);
-                        const tour = DocListCast(target[fieldKey]).reverse();
-                        LightboxView.SetLightboxDoc(currentContext, undefined, tour);
-                        setTimeout(LightboxView.Next);
-                        allFinished();
-                    } else {
-                        const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
-                        const containerDoc = containerAnnoDoc || target;
-                        var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
-                        while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
-                            containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
-                        }
-                        const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
-                        DocumentManager.Instance.jumpToDocument(
-                            target,
-                            zoom,
-                            (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished),
-                            targetContexts,
-                            linkDoc,
-                            undefined,
-                            sourceDoc,
-                            allFinished,
-                            undefined,
-                            undefined,
-                            Cast(sourceDoc.presZoom, 'number', null)
-                        );
-                    }
-                } else {
+            const target = (
+                sourceDoc === linkDoc.anchor1
+                    ? linkDoc.anchor2
+                    : sourceDoc === linkDoc.anchor2
+                    ? linkDoc.anchor1
+                    : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)
+                    ? linkDoc.anchor2
+                    : linkDoc.anchor1
+            ) as Doc;
+            if (target) {
+                const options: DocFocusOptions = {
+                    playAudio: BoolCast(sourceDoc.followLinkAudio),
+                    toggleTarget: BoolCast(sourceDoc.followLinkToggle),
+                    willZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
+                    zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500),
+                    zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null),
+                    effect: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffect) as PresEffect,
+                    effectDirection: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffectDirection) as PresEffectDirection,
+                    originatingDoc: sourceDoc,
+                };
+                if (target.TourMap) {
+                    const fieldKey = Doc.LayoutFieldKey(target);
+                    const tour = DocListCast(target[fieldKey]).reverse();
+                    LightboxView.SetLightboxDoc(currentContext, undefined, tour);
+                    setTimeout(LightboxView.Next);
                     allFinished();
+                } else {
+                    const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
+                    const containerDoc = containerAnnoDoc || target;
+                    var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
+                    while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
+                        containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                    }
+                    const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
+                    DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), targetContexts, allFinished);
                 }
             } else {
                 allFinished();
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 4b0310e76..4cd45fc9c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -392,7 +392,7 @@ export class SharingManager extends React.Component<{}> {
                 onClick={() => {
                     let context: Opt<CollectionView>;
                     if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) {
-                        DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]);
+                        DocumentManager.Instance.jumpToDocument(this.targetDoc, { willZoom: true }, undefined, [context.props.Document]);
                     }
                 }}
                 onPointerEnter={action(() => {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 043a83d16..c9c09b63b 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -147,7 +147,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
                 setTimeout(() =>
                     docs.map(doc => {
                         // this allows 'addDocument' to see the annotationOn field in order to create a pushin
-                        Doc.SetInPlace(doc, 'isPushpin', undefined, true);
+                        Doc.SetInPlace(doc, 'followLinkToggle', undefined, true);
                         doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true);
                     })
                 );
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index aa2818e0d..36ea3ef18 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -41,7 +41,7 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
-import { DocComponentView } from './nodes/DocumentView';
+import { DocComponentView, DocFocusOptions } from './nodes/DocumentView';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { StyleProp } from './StyleProvider';
@@ -83,8 +83,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return this._subContentView?.getAnchor?.() || this.rootDoc;
     };
 
-    scrollFocus = (textAnchor: Doc, smooth: boolean) => {
-        return this._subContentView?.scrollFocus?.(textAnchor, smooth);
+    scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
+        return this._subContentView?.scrollFocus?.(textAnchor, options);
     };
     /**
      * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 165bb69bb..965cd560f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -206,7 +206,7 @@ export class MainView extends React.Component {
         document.addEventListener('dash', (e: any) => {
             // event used by chrome plugin to tell Dash which document to focus on
             const id = FormattedTextBox.GetDocFromUrl(e.detail);
-            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, false, undefined, []) : null));
+            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, { willZoom: false }, undefined, []) : null));
         });
         document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
         this.initEventListeners();
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 1162cde50..07371c9d5 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -92,7 +92,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                 dragComplete: e => {
                     if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
-                        e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+                        e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
                         e.annoDragData.linkSourceDoc.followLinkZoom = false;
                     }
                 },
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 3e199919e..f7cc32cff 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1412,14 +1412,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     changeFollowBehavior = action((follow: string) => this.selectedDoc && (this.selectedDoc.followLinkLocation = follow));
 
     @undoBatch
-    changeAnimationBehavior = action((behavior: string) => this.destinationAnchor && (this.destinationAnchor.presEffect = behavior);
+    changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior));
 
     @undoBatch
-    changeEffectDirection = action((effect: PresEffectDirection) => this.destinationAnchor && (this.destinationAnchor.presEffectDirection = effect);
+    changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.linkAnimDirection = effect));
 
     animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
-        const lanch = this.destinationAnchor;
-        const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.MEDIUM_BLUE : '';
+        const lanch = this.sourceAnchor;
+        const color = lanch?.linkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.linkAnimDirection) ? Colors.MEDIUM_BLUE : '';
         return (
             <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                 <div
@@ -1514,7 +1514,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         if (change) scale += change;
         if (scale < 0.01) scale = 0.01;
         if (scale > 1) scale = 1;
-        this.sourceAnchor && (this.sourceAnchor.presZoom = scale);
+        this.sourceAnchor && (this.sourceAnchor.linkZoomScale = scale);
     };
 
     /**
@@ -1530,7 +1530,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     render() {
         const isNovice = Doc.noviceMode;
-        const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3));
+        const zoom = Number((NumCast(this.sourceAnchor?.linkZoomScale, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
         const indent = 30;
         const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!));
@@ -1621,7 +1621,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         </div>
                                         <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
                                             <p>Animation</p>
-                                            <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.destinationAnchor?.presEffect, 'default')}>
+                                            <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.linkAnimEffect, 'default')}>
                                                 <option value="default">Default</option>
                                                 {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
                                                     <option value={effect.toString()}>{effect.toString()}</option>
@@ -1639,9 +1639,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             '0.1',
                                             '0.1',
                                             '10',
-                                            NumCast(this.destinationAnchor?.presTransition) / 1000,
+                                            NumCast(this.sourceAnchor?.linkTransitionTime) / 1000,
                                             true,
-                                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.destinationAnchor && (this.destinationAnchor.presTransition = timeInMS)),
+                                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.linkTransitionTime = timeInMS)),
                                             indent
                                         )}{' '}
                                         <div
@@ -1668,6 +1668,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>Toggle Target (Show/Hide)</p>
+                                            <button
+                                                style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', 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" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                                             <p>Zoom %</p>
                                             <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 29670a1a7..5e389e17e 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -193,13 +193,12 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             const top = found.getBoundingClientRect().top;
             const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
             }
         }
         const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
         this.props.focus(this.rootDoc, {
-            willZoom: options?.willZoom,
-            scale: options?.scale,
+            ...options,
             afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
         });
     };
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ac6391365..08aebc62d 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -255,13 +255,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             const top = found.getBoundingClientRect().top;
             const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
             }
         }
         const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing;
         this.props.focus(this.rootDoc, {
-            willZoom: options?.willZoom,
-            scale: options?.scale,
+            ...options,
             afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
         });
     };
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 7696777fd..13984171c 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -297,7 +297,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
             if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
             CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
-            setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+            setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), { willZoom: false }, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
         }
         setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
     }
@@ -376,7 +376,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     focusFunc = (doc: Doc, options: DocFocusOptions) => {
         const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap;
         if (options?.willZoom !== false && shrinkwrap && this._document) {
-            const focusSpeed = NumCast(this._document.focusSpeed, 500);
+            const focusSpeed = options.zoomTime ?? 500;
             shrinkwrap();
             this._document._viewTransition = `transform ${focusSpeed}ms`;
             setTimeout(
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 56a5c3dcc..5fb3c1ac6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1164,7 +1164,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined };
             const newState = HistoryUtil.getState();
             const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc;
-            const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined);
+            const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.zoomScale || 0.75 : undefined);
             if (!cantTransform) {
                 // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
                 newState.initializers![this.Document[Id]] = { panX, panY };
@@ -1172,7 +1172,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
             // focus on the document in the collection
             const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
-            const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0;
+            const focusSpeed = options?.instant ? 0 : didMove ? options.zoomTime ?? 500 : 0;
             // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
             if (didMove) {
                 scale && (this.Document[this.scaleFieldKey] = scale);
@@ -1538,8 +1538,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    scrollFocus = (anchor: Doc, smooth: boolean) => {
-        const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition);
+    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
+        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
             this.rootDoc, //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3fd6ca803..9df3e195f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -541,7 +541,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             d.y = NumCast(d.y) - this.Bounds.top;
             return d;
         });
-        const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
+        const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
         const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
         DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', '');
 
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 9778fc4fe..924e07daa 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -150,7 +150,7 @@ export class CollectionLinearView extends CollectionSubView() {
                 <span className="bottomPopup-text">
                     Currently playing:
                     {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => (
-                        <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, true, undefined, [])}>
+                        <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, { willZoom: true }, undefined, [])}>
                             {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')}
                         </span>
                     ))}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index a22999f52..c0dfeedfa 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -214,7 +214,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
             const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
             // Jump to the this document
-            DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
+            DocumentManager.Instance.jumpToDocument(this._rowDoc, { willZoom: false }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc));
         }
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 48e32c071..fb20887cb 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -53,6 +53,7 @@ import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
+import { PresEffect, PresEffectDirection } from './trails';
 const { Howl } = require('howler');
 
 interface Window {
@@ -95,10 +96,17 @@ export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that
 export interface DocFocusOptions {
     originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
     willZoom?: boolean; // determines whether to zoom in on target document
-    scale?: number; // percent of containing frame to zoom into document
+    zoomScale?: number; // percent of containing frame to zoom into document
+    zoomTime?: number;
     afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
     docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
     instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+    effect?: PresEffect; // animation effect for focus
+    effectDirection?: PresEffectDirection;
+    noSelect?: boolean; // whether target should be selected after focusing
+    playAudio?: boolean; // whether to play audio annotation on focus
+    toggleTarget?: boolean; // whether to toggle target on and off
+    originatingDoc?: Doc; // document that triggered the focus
 }
 export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
 export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
@@ -106,7 +114,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p
 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)
-    scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus
+    scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
     setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
@@ -566,7 +574,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec])));
         // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs
         setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
-        const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant !== true && !LinkDocPreview.LinkInfo);
+        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;
         this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
             ...options,
@@ -741,8 +749,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => {
         this.Document.ignoreClick = false;
         if (setPushpin) {
-            this.Document.isPushpin = !this.Document.isPushpin;
-            this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton;
+            this.Document.followLinkToggle = !this.Document.followLinkToggle;
+            this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton;
         } else {
             this.Document._isLinkButton = !this.Document._isLinkButton;
         }
@@ -759,14 +767,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     toggleTargetOnClick = (): void => {
         this.Document.ignoreClick = false;
         this.Document._isLinkButton = true;
-        this.Document.isPushpin = true;
+        this.Document.followLinkToggle = true;
     };
     @undoBatch
     @action
     followLinkOnClick = (location: Opt<string>, zoom: boolean): void => {
         this.Document.ignoreClick = false;
         this.Document._isLinkButton = true;
-        this.Document.isPushpin = false;
+        this.Document.followLinkToggle = false;
         this.Document.followLinkZoom = zoom;
         this.Document.followLinkLocation = location;
     };
@@ -775,7 +783,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     selectOnClick = (): void => {
         this.Document.ignoreClick = false;
         this.Document._isLinkButton = false;
-        this.Document.isPushpin = false;
+        this.Document.followLinkToggle = false;
         this.Document.onClick = this.layoutDoc.onClick = undefined;
     };
     @undoBatch
@@ -934,7 +942,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.isPushpin ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
+                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Pushpin', 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) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 1d79febdf..416107859 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -31,7 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import React = require('react');
 import { PresBox } from './trails';
-import { DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentViewProps } from './DocumentView';
 
 export const pageSchema = createSchema({
     googlePhotosUrl: 'string',
@@ -62,8 +62,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     };
 
     @action
-    scrollFocus = (anchor: Doc, smooth: boolean) => {
-        const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition);
+    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
+        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
             this.rootDoc, //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
@@ -168,7 +168,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const cropping = Doc.MakeCopy(region, true);
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
-        Doc.GetProto(region).isPushpin = true;
+        Doc.GetProto(region).followLinkToggle = true;
         this.addDocument(region);
         const anchx = NumCast(cropping.x);
         const anchy = NumCast(cropping.y);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 39e323247..b19c4a9e2 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -28,6 +28,7 @@ import './PDFBox.scss';
 import { VideoBox } from './VideoBox';
 import React = require('react');
 import { PresBox } from './trails';
+import { DocFocusOptions } from './DocumentView';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -94,7 +95,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const cropping = Doc.MakeCopy(region, true);
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
-        Doc.GetProto(region).isPushpin = true;
+        Doc.GetProto(region).followLinkToggle = true;
         this.addDocument(region);
 
         const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
@@ -200,16 +201,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => {
         this._pdfViewer?.brushView(view);
     };
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
-            this.toggleSidebar(!smooth);
+            this.toggleSidebar(!options.instant);
             didToggle = true;
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, NumCast(doc.presTransition, NumCast(doc.focusSpeed, 500)), { pannable: doc.presPinData ? true : false });
-        return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), smooth) ?? (didToggle ? 1 : undefined);
+        PresBox.restoreTargetDocView(this.rootDoc, {}, 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 = () => {
         const docAnchor = () => {
@@ -277,7 +278,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     setPdfViewer = (pdfViewer: PDFViewer) => {
         this._pdfViewer = pdfViewer;
         if (this._initialScrollTarget) {
-            this.scrollFocus(this._initialScrollTarget, false);
+            this.scrollFocus(this._initialScrollTarget, { instant: true });
             this._initialScrollTarget = undefined;
         }
     };
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 82d5b00f9..607cd6187 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -31,7 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 import { ObjectField } from '../../../fields/ObjectField';
-import { OpenWhere } from './DocumentView';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
 const path = require('path');
 
 /**
@@ -950,7 +950,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     };
 
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
         if (doc !== this.rootDoc) {
             const showTime = Cast(doc._timecodeToShow, 'number', null);
             showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
@@ -1006,7 +1006,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Doc.GetProto(region).backgroundColor = 'transparent';
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
-        Doc.GetProto(region).isPushpin = true;
+        Doc.GetProto(region).followLinkToggle = true;
         region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
         this.addDocument(region);
         const anchx = NumCast(cropping.x);
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 5ce6a0eb1..64b186489 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 { DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
@@ -284,17 +284,17 @@ 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, smooth: boolean) => {
-        if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
+    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
+        if (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(!smooth);
+            this.toggleSidebar(options.instant);
         }
         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()));
             if (scrollTo !== undefined && this._initialScroll === undefined) {
-                const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0;
+                const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
                 this.goTo(scrollTo, focusSpeed);
                 return focusSpeed;
             } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ce4639b76..9e91f6c46 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { StyleProp } from '../../StyleProvider';
-import { DocumentViewInternal, OpenWhere } from '../DocumentView';
+import { DocFocusOptions, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
 import { DashDocCommentView } from './DashDocCommentView';
@@ -689,9 +689,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {});
 
     @undoBatch
-    makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin);
+    makePushpin = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle);
 
-    isPushpin = (anchor: Doc) => BoolCast(anchor.isPushpin);
+    isPushpin = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
 
     specificContextMenu = (e: React.MouseEvent): void => {
         const cm = ContextMenu.Instance;
@@ -922,10 +922,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return anchorDoc ?? this.rootDoc;
     }
 
-    scrollFocus = (textAnchor: Doc, smooth: boolean) => {
+    scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
-            this.toggleSidebar(!smooth);
+            this.toggleSidebar(options.instant);
             didToggle = true;
         }
         const textAnchorId = textAnchor[Id];
@@ -966,7 +966,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
             const content = (ret.frag as any)?.content;
             if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
-                smooth && (this._focusSpeed = 500);
+                !options.instant && (this._focusSpeed = 500);
                 let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
                 if (ret.frag.firstChild) {
                     selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index c04b79a1e..4073677f3 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -15,7 +15,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty
 import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, 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';
@@ -35,7 +35,7 @@ import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import { map } from 'bluebird';
-import { OpenWhere, OpenWhereMod } from '../DocumentView';
+import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -313,6 +313,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
+            let focusSpeed = 500;
             if (activeItem.presActiveFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
                 const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
@@ -334,11 +335,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
                 this.startTempMedia(targetDoc, activeItem);
             }
-            if (targetDoc) {
-                //  Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
-                targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500)));
-                setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10);
-            }
             if (targetDoc?.lastFrame !== undefined) {
                 targetDoc._currentFrame = 0;
             }
@@ -583,8 +579,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
         } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
             LightboxView.SetLightboxDoc(undefined);
-            const zooming = activeItem.presMovement !== PresMovement.Pan;
-            DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, activeItem, finished, undefined, true, NumCast(activeItem.presZoom, 1));
+            const options: DocFocusOptions = {
+                willZoom: activeItem.presMovement !== PresMovement.Pan,
+                zoomScale: NumCast(activeItem.presZoom, 1),
+                zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                noSelect: true,
+                originatingDoc: activeItem,
+            };
+            DocumentManager.Instance.jumpToDocument(targetDoc, options, openInTab, srcContext ? [srcContext] : [], finished);
         } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
         }
@@ -1199,7 +1201,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     _batch: UndoManager.Batch | undefined = undefined;
 
-    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?:number) => {
+    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => {
         let batch: any;
         return (
             <input
@@ -1208,7 +1210,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 min={min}
                 max={max}
                 value={value}
-                style={{marginLeft: hmargin, marginRight:hmargin, width: `calc(100% - ${2*(hmargin??0)}px)`}}
+                style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
                     batch = UndoManager.StartBatch('pres slider');
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 133b882f6..7069ff399 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.isPushpin = !this.annoTextRegion.isPushpin);
+    makePushpin = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
 
-    isPushpin = () => BoolCast(this.annoTextRegion.isPushpin);
+    isPushpin = () => BoolCast(this.annoTextRegion.followLinkToggle);
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5a5c63c3d..0703ca9b4 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -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 { DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { StyleProp } from '../StyleProvider';
@@ -166,7 +166,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     // scrolls to focus on a nested annotation document.  if this is part a link preview then it will jump to the scroll location,
     // otherwise it will scroll smoothly.
-    scrollFocus = (doc: Doc, scrollTop: number, smooth: boolean) => {
+    scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => {
         const mainCont = this._mainCont.current;
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
@@ -174,7 +174,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = scrollTo;
-                else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo);
+                else if (!options.instant) smoothScroll((focusSpeed = options.zoomTime??500), mainCont, scrollTo);
                 else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
             }
         } else {
@@ -295,7 +295,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
         if (scrollToAnnotation) {
-            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), true);
+            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), {zoomTime: 500});
             Doc.linkFollowHighlight(scrollToAnnotation);
         }
     };
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index c5177de90..9c656e093 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -407,7 +407,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
      * or opening it in a new tab.
      */
     selectElement = async (doc: Doc, finishFunc: () => void) => {
-        await DocumentManager.Instance.jumpToDocument(doc, true, undefined, [], undefined, undefined, undefined, finishFunc);
+        await DocumentManager.Instance.jumpToDocument(doc, { willZoom: true }, undefined, [], finishFunc);
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 613daac016c367205ff1afddd81b7b9111c52d33 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 Dec 2022 22:45:22 -0500
Subject: cleaning up following links and pres item following so that view
 transitions don't interfere when clicking quickly (eg through animation
 frames).  changed animations to animate multi-level zooming parallel.

---
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/util/DocumentManager.ts                 | 145 ++++++------
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/views/LightboxView.tsx                  |   8 +-
 src/client/views/PropertiesButtons.tsx             |   8 +-
 src/client/views/PropertiesView.tsx                |   5 +-
 .../views/collections/CollectionDockingView.tsx    |   3 +-
 src/client/views/collections/CollectionMenu.tsx    |  11 +-
 src/client/views/collections/TabDocView.tsx        |  16 +-
 .../CollectionFreeFormLinkView.tsx                 |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  39 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  14 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  57 ++---
 src/client/views/nodes/DocumentView.tsx            |  54 ++++-
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |   2 +-
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          | 253 ++++++---------------
 19 files changed, 280 insertions(+), 357 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d13209c4f..9dfc91e3f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -667,7 +667,7 @@ export class CurrentUserUtils {
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20,  scripts: {}},
+            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()', scripts: { script: 'return curKeyFrame(_readOnly_)'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 1b63b615b..d2368f4f6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,19 +1,19 @@
 import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { Cast, DocCast } from '../../fields/Types';
+import { listSpec } from '../../fields/Schema';
+import { Cast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
-import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 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 { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
-import { listSpec } from '../../fields/Schema';
-import { AudioField } from '../../fields/URLField';
 const { Howl } = require('howler');
 
 export class DocumentManager {
@@ -31,6 +31,30 @@ export class DocumentManager {
     //private constructor so no other class can create a nodemanager
     private constructor() {}
 
+    private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
+    public AddViewRenderedCb = (doc: Doc, func: (dv: DocumentView) => any) => {
+        const dv = this.getDocumentViewById(doc[Id]);
+        this._viewRenderedCbs.push({ doc, func });
+        if (dv) {
+            this.callAddViewFuncs(dv);
+        }
+    };
+    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));
+            const intTimer = setInterval(
+                () => {
+                    if (!view.ComponentView?.incrementalRendering?.()) {
+                        callFuncs.forEach(cf => cf.func(view));
+                        clearInterval(intTimer);
+                    }
+                },
+                view.ComponentView?.incrementalRendering?.() ? 0 : 100
+            );
+        }
+    };
+
     @action
     public AddView = (view: DocumentView) => {
         //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
@@ -50,6 +74,7 @@ export class DocumentManager {
         } else {
             this.DocumentViews.add(view);
         }
+        this.callAddViewFuncs(view);
     };
     public RemoveView = action((view: DocumentView) => {
         this.LinkedDocumentViews.slice().forEach(
@@ -173,7 +198,7 @@ export class DocumentManager {
         targetDoc: Doc, // document to display
         options: DocFocusOptions, // options for how to navigate to target
         createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
-        docContext: Doc[], // context to load that should contain the target
+        docContextPath: Doc[], // context to load that should contain the target
         finished?: () => void
     ): void => {
         const originalTarget = options.originalTarget ?? targetDoc;
@@ -201,29 +226,20 @@ export class DocumentManager {
             finished?.();
         };
         const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
-        const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
-        const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
-        const targetDocContext = contextDoc || annotatedDoc;
-        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 (annoContainerView) {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
-                annoContainerView.iconify(() =>
-                    annoContainerView.focus(targetDoc, {
-                        ...options,
-                        originalTarget,
-                        afterFocus: (didFocus: boolean) =>
-                            new Promise<ViewAdjustment>(res => {
-                                focusAndFinish(true);
-                                res(ViewAdjustment.doNothing);
-                            }),
-                    })
-                );
-                return;
-            } else if (!docView && targetDoc.type !== DocumentType.MARKER) {
+                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options, 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
             }
         }
+
+        const contextDoc = docContextPath.length ? docContextPath[0] : undefined;
+        const remainingDocContext = docContextPath.length ? docContextPath.slice(1) : [];
+        const targetDocContext = contextDoc || annotatedDoc;
+        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, targetDoc); //TODO:glr make this a setting in PresBox
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
@@ -252,56 +268,53 @@ export class DocumentManager {
                     // we found a context view and aren't forced to create a new one ... focus on the context first..
                     wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
                     targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
-                    targetDocContext._viewTransition = 'transform 500ms';
+
+                    if (targetDocContext.layoutKey === 'layout_icon') {
+                        return targetDocContextView.iconify(
+                            () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)),
+                            30
+                        );
+                    }
+
+                    const contextFocusTime = options.zoomTime ? options.zoomTime / 2 : 500;
+                    const remainingFocustime = options.zoomTime ? options.zoomTime - contextFocusTime : undefined;
+                    targetDocContextView.setViewTransition('transform', contextFocusTime);
+                    // this makes focusing on contexts run in parallel -- jutmp to document below makes them run sequentially
+                    this.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished));
                     targetDocContextView.props.focus(targetDocContextView.rootDoc, {
                         ...options,
+                        zoomTime: contextFocusTime,
                         // originalTarget, // needed?
                         afterFocus: async () => {
-                            targetDocContext._viewTransition = undefined;
-                            if (targetDocContext.layoutKey === 'layout_icon') {
-                                targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished));
+                            // now find the target document within the context
+                            if (targetDoc._timecodeToShow) {
+                                // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
+                                targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
+                                finished?.();
+                            } else {
+                                // otherwise, just look for the target document in this context view now that we've focused the context view
+                                if (this.getFirstDocumentView(resolvedTarget)) {
+                                    // test again for the target view snce we presumably created the context above by focusing on it
+                                    this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished);
+                                } else if (targetDoc.layout) {
+                                    // there will no layout for a TEXTANCHOR type document
+                                    createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
+                                }
                             }
                             return ViewAdjustment.doNothing;
                         },
                     });
-
-                    // now find the target document within the context
-                    if (targetDoc._timecodeToShow) {
-                        // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
-                        targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
-                        finished?.();
-                    } else {
-                        // no timecode means we need to find the context view and focus on our target
-                        const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
-                        if (retryDocView) {
-                            // we found the target in the context.
-                            Doc.linkFollowHighlight(retryDocView.rootDoc);
-                            retryDocView.focus(targetDoc, {
-                                ...options,
-                                // originalTarget -- needed?
-                                afterFocus: (didFocus: boolean) =>
-                                    new Promise<ViewAdjustment>(res => {
-                                        !options.noSelect && focusAndFinish(true);
-                                        res(ViewAdjustment.doNothing);
-                                    }),
-                            }); // focus on the target in the context
-                        } else if (targetDoc.layout) {
-                            // there will no layout for a TEXTANCHOR type document
-                            createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
-                        }
-                    }
                 } else {
-                    if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
-                        const docContextView = this.getFirstDocumentView(docContext[0]);
-                        if (docContextView) {
-                            return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished));
-                        }
+                    if (docContextPath.length && docContextPath[0]?.layoutKey === 'layout_icon') {
+                        Doc.deiconifyView(docContextPath[0]);
+                        this.jumpToDocument(targetDoc, options, createViewFunc, docContextPath, finished);
+                    } else {
+                        // there's no context view so we need to create one first and try again when that finishes
+                        createViewFunc(
+                            targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
+                            () => this.jumpToDocument(targetDoc, { ...options }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), remainingDocContext, finished)
+                        );
                     }
-                    // there's no context view so we need to create one first and try again when that finishes
-                    createViewFunc(
-                        targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
-                        () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished)
-                    );
                 }
             }
         }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 4f742817a..aec4db1df 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -48,7 +48,7 @@ export class LinkFollower {
                             });
                         } else {
                             finished?.();
-                            res(where !== 'inPlace' || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
+                            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
                         }
                     }, 100);
                 });
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 1f58763d1..cf0a2fcfb 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -50,7 +50,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this.LightboxDoc._panY = this._savedState.panY;
             this.LightboxDoc._scrollTop = this._savedState.scrollTop;
             this.LightboxDoc._viewScale = this._savedState.scale;
-            this.LightboxDoc._viewTransition = undefined;
         }
         if (!doc) {
             this._docFilters && (this._docFilters.length = 0);
@@ -167,7 +166,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (targetDocView && target) {
             const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
-            targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
+            targetDocView.focus(target, { originalTarget: target, willZoom: true, zoomScale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
         } else {
             if (!target && LightboxView.path.length) {
@@ -177,7 +176,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     LightboxView.LightboxDoc._panY = saved.panY;
                     LightboxView.LightboxDoc._viewScale = saved.scale;
                     LightboxView.LightboxDoc._scrollTop = saved.scrollTop;
-                    LightboxView.LightboxDoc._viewTransition = undefined;
                 }
                 const pop = LightboxView.path.pop();
                 if (pop) {
@@ -211,7 +209,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (docView) {
             LightboxView._docTarget = target;
             if (!target) docView.ComponentView?.shrinkWrap?.();
-            else docView.focus(target, { willZoom: true, scale: 0.9 });
+            else docView.focus(target, { willZoom: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
@@ -290,7 +288,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                                             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?.();
+                                            //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
                                         })
                                     );
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 656a56a15..66c3ed439 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -14,7 +14,7 @@ import { SelectionManager } from '../util/SelectionManager';
 import { undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { VideoBox } from './nodes/VideoBox';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
@@ -117,7 +117,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             'In Place',
             'isInPlaceContainer',
             on => `${on ? 'Make' : 'Remove'} in place container flag`,
-            on => 'window',
+            on => 'window-restore',
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
@@ -129,7 +129,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         containerDoc._forceActive =
                         containerDoc._isInPlaceContainer =
                             !containerDoc._isInPlaceContainer;
-                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? 'inPlace' : undefined;
+                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? OpenWhere.inPlace : undefined;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
                     const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
                     if (menuDoc) {
@@ -139,7 +139,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         }
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
                             menuItem.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true;
-                            menuItem._followLinkLocation = 'inPlace';
+                            menuItem._followLinkLocation = OpenWhere.inPlace;
                         });
                     }
                 });
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f7cc32cff..bc08e920a 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1409,7 +1409,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     });
 
     @undoBatch
-    changeFollowBehavior = action((follow: string) => this.selectedDoc && (this.selectedDoc.followLinkLocation = follow));
+    changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior));
@@ -1612,7 +1612,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <option value="add:right">Opening in new right pane</option>
                                                 <option value="replace:left">Replacing left tab</option>
                                                 <option value="replace:right">Replacing right tab</option>
-                                                <option value="fullScreen">Opening full screen</option>
+                                                <option value="fullScreen">Overlaying current tab</option>
+                                                <option value="lightbox">Opening in lightbox</option>
                                                 <option value="add">Opening in new tab</option>
                                                 <option value="replace">Replacing current tab</option>
                                                 <option value="inPlace">Opening in place</option>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 8cbe548c7..ffc004df6 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -191,7 +191,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
         if (!pullSide && stack) {
             stack.addChild(docContentConfig, undefined);
-            stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]);
+            setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
         } else {
             const newContentItem = () => {
                 const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout);
@@ -389,6 +389,7 @@ export class CollectionDockingView extends CollectionSubView() {
                 const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
                 if (!className.includes('lm_close') && !className.includes('lm_maximise')) {
                     this._flush = UndoManager.StartBatch('golden layout edit');
+                    DocServer.UPDATE_SERVER_CACHE();
                 }
             }
         }
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index db81f28f6..e2594b6ae 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -31,7 +31,7 @@ import { Colors } from '../global/globalEnums';
 import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocumentView } from '../nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { DefaultStyleProvider } from '../StyleProvider';
@@ -569,7 +569,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     startRecording = () => {
         const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
         //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
-        CollectionDockingView.AddSplit(doc, 'right');
+        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
     };
 
     @computed
@@ -733,11 +733,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                 doc._currentFrame = 0;
                 CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
             }
-            CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
+            CollectionFreeFormDocumentView.updateKeyframe(undefined, childDocs, currentFrame || 0);
             doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
         }
     }
 
+    _keyTimer: NodeJS.Timeout | undefined;
     @undoBatch
     @action
     nextKeyframe = (): void => {
@@ -746,7 +747,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+        this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
         this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame));
     };
@@ -758,7 +759,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
         this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
     };
 
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 13984171c..af1fb175a 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -196,11 +196,12 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc),
                 action(selected => {
                     if (selected) this._activated = true;
-                    const toggle = tab.element[0].children[1].children[0] as HTMLInputElement;
+                    const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
                     selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
-                    // toggle.style.fontWeight = selected ? "bold" : "";
+                    toggle.style.fontWeight = selected ? 'bold' : '';
                     // toggle.style.textTransform = selected ? "uppercase" : "";
-                })
+                }),
+                { fireImmediately: true }
             );
 
             // highlight the tab when the tab document is brushed in any part of the UI
@@ -378,14 +379,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         if (options?.willZoom !== false && shrinkwrap && this._document) {
             const focusSpeed = options.zoomTime ?? 500;
             shrinkwrap();
-            this._document._viewTransition = `transform ${focusSpeed}ms`;
-            setTimeout(
-                action(() => {
-                    this._document!._viewTransition = undefined;
-                    options?.afterFocus?.(false);
-                }),
-                focusSpeed
-            );
+            this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false));
         } else {
             options?.afterFocus?.(false);
         }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index be20bf207..c57810a98 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -270,12 +270,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     </marker>
                     <filter id="outline">
                         <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" />
-                        <feFlood flood-color={`${Colors.DARK_GRAY}`} />
+                        <feFlood floodColor={`${Colors.DARK_GRAY}`} />
                         <feComposite in2="expanded" operator="in" />
                         <feComposite in="SourceGraphic" />
                     </filter>
                     <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}>
-                        <feFlood flood-color={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
+                        <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
                         <feMerge>
                             <feMergeNode in="bg" />
                             <feMergeNode in="SourceGraphic" />
@@ -286,7 +286,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     filter={LinkManager.currentLink === link ? 'url(#outline)' : ''}
                     fill="pink"
                     stroke="antiquewhite"
-                    stroke-width="4"
+                    strokeWidth="4"
                     className="collectionfreeformlinkview-linkLine"
                     style={{ pointerEvents: 'all', opacity: this._opacity, stroke, strokeWidth }}
                     onClick={this.onClickLine}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5fb3c1ac6..b8ff59a14 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,5 @@
 import { Bezier } from 'bezier-js';
+import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
@@ -47,7 +48,7 @@ import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { TabDocView } from '../TabDocView';
-import { computePivotLayout, computePassLayout as computePassLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
@@ -108,7 +109,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
-    @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
+    @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _hLines: number[] | undefined;
     @observable _vLines: number[] | undefined;
     @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
@@ -176,6 +177,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
     }
 
+    _keyTimer: NodeJS.Timeout | undefined;
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
@@ -183,14 +185,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         if (back) {
-            CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+            this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
         } else {
-            CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+            this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
     };
+    @observable _keyframeEditing = false;
+    @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
+    getKeyFrameEditing = () => this._keyframeEditing;
     onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
     onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
@@ -712,6 +717,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     pan = (e: PointerEvent | React.Touch | { clientX: number; clientY: number }): void => {
+        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._lastX = e.clientX;
@@ -1079,7 +1085,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
-            this._viewTransition = panTime;
+            this._panZoomTransition = panTime;
             const scale = this.getLocalTransform().inverse().Scale;
             const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
             const minPanX = NumCast(this.rootDoc._panXMin, 0);
@@ -1098,7 +1104,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true);
             this._lastNudge && clearTimeout(this._lastNudge);
             this._lastNudge = setTimeout(
-                action(() => (this._viewTransition = 0)),
+                action(() => (this._panZoomTransition = 0)),
                 nudgeTime
             );
             return true;
@@ -1127,7 +1133,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
         if (this.Document._isGroup) return;
-        this._viewTransition = transitionTime;
+        this._panZoomTransition = transitionTime;
         const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         this.layoutDoc[this.scaleFieldKey] = scale;
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
@@ -1135,7 +1141,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         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];
-        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._viewTransition = 0))), this._viewTransition)); // set transition to be smooth, then reset
+        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset
     }
 
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
@@ -1185,14 +1191,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 doc.hidden && Doc.UnHighlightDoc(doc);
                 const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing;
                 if (resetView) {
-                    const restoreState = (!LightboxView.LightboxDoc || LightboxView.LightboxDoc === this.props.Document) && savedState;
+                    const restoreState = savedState;
                     if (typeof restoreState !== 'boolean') {
                         this.Document._panX = restoreState.panX;
                         this.Document._panY = restoreState.panY;
                         this.Document[this.scaleFieldKey] = restoreState.scale;
                     }
                 }
-                runInAction(() => (this._viewTransition = 0));
+                runInAction(() => (this._panZoomTransition = 0));
                 return resetView;
             };
             const xf = !cantTransform
@@ -1364,7 +1370,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             z: Cast(z, 'number'),
             color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
             backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor),
-            opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
+            opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
             width: Cast(childDocLayout._width, 'number'),
             height: Cast(childDocLayout._height, 'number'),
@@ -1541,7 +1547,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
-            this.rootDoc, //
+            this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
             focusSpeed,
@@ -1869,6 +1875,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         DragManager.SetSnapLines(horizLines, vertLines);
     };
 
+    incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
+
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
             const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
@@ -1944,7 +1952,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         zoomScaling={this.zoomScaling}
                         presPaths={this.showPresPaths}
                         presPinView={BoolCast(this.Document.presPinView)}
-                        transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
+                        transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
                         viewDefDivClick={this.props.viewDefDivClick}>
                         {this.children}
                     </CollectionFreeFormViewPannableContents>
@@ -2265,6 +2273,11 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
+ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
+    const selView = SelectionManager.Views();
+    if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : undefined;
+    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()) }));
 });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 9df3e195f..7c1137292 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,6 @@ import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { OpenWhere } from '../../nodes/DocumentView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
 import { VideoBox } from '../../nodes/VideoBox';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { PreviewCursor } from '../../PreviewCursor';
@@ -244,7 +243,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         if (!(e.nativeEvent as any).marqueeHit) {
             (e.nativeEvent as any).marqueeHit = true;
             // allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode
-            if (e.button === 2 || (e.button === 0 && e.altKey) || (PresBox.startMarquee && e.button === 0)) {
+            if (e.button === 2 || (e.button === 0 && e.altKey)) {
                 // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
                 this.setPreviewCursor(e.clientX, e.clientY, true, false);
                 // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
@@ -273,9 +272,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this
         }
         e.altKey && e.preventDefault();
-        if (PresBox.startMarquee) {
-            e.stopPropagation();
-        }
     };
 
     @action
@@ -296,11 +292,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             document.removeEventListener('pointerdown', hideMarquee, true);
             document.removeEventListener('wheel', hideMarquee, true);
         };
-        if (PresBox.startMarquee) {
-            this.pinWithView();
-            PresBox.startMarquee = false;
-        }
-        if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100 && !PresBox.startMarquee) {
+        if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100) {
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
             MarqueeOptionsMenu.Instance.delete = this.delete;
             MarqueeOptionsMenu.Instance.summarize = this.summary;
@@ -686,7 +678,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 className="marqueeView"
                 style={{
                     overflow: StrCast(this.props.Document._overflow),
-                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible || PresBox.startMarquee ? 'crosshair' : 'pointer',
+                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
                 }}
                 onDragOver={e => e.preventDefault()}
                 onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 868822fbf..bf1f13a06 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -115,45 +115,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         });
     }
 
-    public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) {
+    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number, targetDoc?: Doc) {
+        if (timer) clearTimeout(timer);
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
         const timecode = Math.round(time);
-        docs.forEach(
-            action(doc => {
-                doc._viewTransition = doc.dataTransition = 'all 1s';
-                CollectionFreeFormDocumentView.animFields.forEach(val => {
-                    const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
-                });
-                CollectionFreeFormDocumentView.animStringFields.forEach(val => {
-                    const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
-                });
-                CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
-                    const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any);
-                });
-            })
-        );
-        setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    doc.dataTransition = 'inherit';
-                }),
-            1010
-        );
+        docs.forEach(doc => {
+            CollectionFreeFormDocumentView.animFields.forEach(val => {
+                const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
+            });
+            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+            });
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+            });
+        });
+        return newTimer;
     }
 
-    public static gotoKeyframe(docs: Doc[], duration = 1000) {
-        docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`));
-        setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    doc.dataTransition = 'inherit';
-                }),
-            1010
-        );
+    public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration = 1000) {
+        if (timer) clearTimeout(timer);
+        return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
     }
 
     public static setupZoom(doc: Doc, targDoc: Doc) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb20887cb..4abfef563 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -127,6 +127,7 @@ export interface DocComponentView {
     Pause?: () => void;
     setFocus?: () => void;
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+    incrementalRendering?: () => void;
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
@@ -238,7 +239,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
 @observer
 export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
-    _animateScaleTime = 300; // milliseconds;
+    @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
     @observable _pendingDoubleClick = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -259,6 +260,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private get topMost() {
         return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
     }
+    public get animateScaleTime() {
+        return this._animateScaleTime ?? 300;
+    }
     public get displayName() {
         return 'DocumentView(' + this.props.Document.title + ')';
     } // this makes mobx trace() statements more descriptive
@@ -1396,7 +1400,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
                         fontSize: Cast(this.Document._fontSize, 'string', null),
                         transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
-                        transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+                        transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
                     }}>
                     {this.innards}
                     {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
@@ -1476,7 +1480,37 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return 'DocumentView(' + this.props.Document?.title + ')';
     } // this makes mobx trace() statements more descriptive
     public ContentRef = React.createRef<HTMLDivElement>();
+    public ViewTimer: NodeJS.Timeout | undefined; // timer for res
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    public clearViewTransition = () => {
+        this.ViewTimer && clearTimeout(this.ViewTimer);
+        this.rootDoc._viewTransition = undefined;
+    };
+    public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
+        this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`;
+        if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`;
+        this.ViewTimer && clearTimeout(this.ViewTimer);
+        return (this.ViewTimer = setTimeout(() => {
+            this.rootDoc._viewTransition = undefined;
+            this.rootDoc._dataTransition = 'inherit';
+            afterTrans?.();
+        }, timeInMs + 10));
+    };
+    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) {
+        docs.forEach(doc => {
+            doc._viewTransition = `${transProp} ${timeInMs}ms`;
+            dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
+        });
+        return setTimeout(
+            () =>
+                docs.forEach(doc => {
+                    doc._viewTransition = undefined;
+                    dataTrans && (doc.dataTransition = 'inherit');
+                    afterTrans?.();
+                }),
+            timeInMs + 10
+        );
+    }
 
     public static showBackLinks(linkSource: Doc) {
         const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
@@ -1617,15 +1651,21 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
     };
 
-    public iconify(finished?: () => void) {
+    public iconify(finished?: () => void, animateTime?: number) {
         this.ComponentView?.updateIcon?.();
+        const animTime = this.docView?._animateScaleTime;
+        runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime));
+        const finalFinished = action(() => {
+            finished?.();
+            this.docView && (this.docView._animateScaleTime = animTime);
+        });
         const layoutKey = Cast(this.Document.layoutKey, 'string', null);
         if (layoutKey !== 'layout_icon') {
-            this.switchViews(true, 'icon', finished);
+            this.switchViews(true, 'icon', finalFinished);
             if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', '');
         } else {
             const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
-            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
+            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
             this.Document.deiconifyLayout = undefined;
             this.props.bringToFront(this.rootDoc);
         }
@@ -1651,10 +1691,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         this.docView && (this.docView._animateScalingTo = 0);
                         finished?.();
                     }),
-                    this.docView!._animateScaleTime - 10
+                    this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0
                 );
             }),
-            this.docView!._animateScaleTime - 10
+            this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0
         );
     });
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 416107859..19f5e9e29 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -65,7 +65,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
-            this.rootDoc, //
+            this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
             focusSpeed,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b19c4a9e2..88aac67a7 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -209,7 +209,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
+        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 = () => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 607cd6187..a8f78edd5 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -961,7 +961,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
-            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
                 <CollectionStackedTimeline
                     ref={action((r: any) => (this._stackedTimeline = r))}
                     {...this.props}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index fe8c85e5e..e477d7ae2 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -510,8 +510,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.EditableText: return this.editableText;
             case ButtonType.DropdownButton: button = this.dropdownButton; break;
             case ButtonType.ToggleButton: button = this.toggleButton; break;
-            case ButtonType.TextButton: button = (
-                    <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
+            case ButtonType.TextButton: 
+                    // Script for checking the outcome of the toggle
+                    const script = ScriptCast(this.rootDoc.script);
+                    const checkResult = script?.script.run({ _readOnly_: true }).result;
+                    button = (
+                    <div className={`menuButton ${this.type}`} style={{ color, backgroundColor:checkResult ?? backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
                         {this.Icon(color)}
                         {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
                         {label()}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4073677f3..169f51dac 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -5,7 +5,7 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
 import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -13,9 +13,9 @@ import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { AudioField } from '../../../../fields/URLField';
-import { emptyFunction, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocumentOptions } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -30,12 +30,11 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { map } from 'bluebird';
-import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -97,7 +96,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public selectedArray = new ObservableSet<Doc>();
 
     @observable public static Instance: PresBox;
-    @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
 
     @observable _isChildActive = false;
     @observable _moveOnFromAudio: boolean = true;
@@ -140,7 +138,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
     }
     @computed get scrollable() {
-        if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
+        if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
         return false;
     }
     @computed get panable() {
@@ -214,16 +212,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         PresBox.Instance = this;
     };
 
-    // There are still other internal frames and should go through all frames before going to next slide
-    nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => {
-        const currentFrame = Cast(targetDoc?._currentFrame, 'number', null);
-        const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
-        targetDoc._viewTransition = 'all 1s';
-        setTimeout(() => (targetDoc._viewTransition = undefined), 1010);
-        this.nextKeyframe(targetDoc, activeItem);
-        targetDoc.keyFrameEditing = true;
-    };
-
     _mediaTimer!: [NodeJS.Timeout, Doc];
     // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
     startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
@@ -279,23 +267,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const targetDoc: Doc = this.targetDoc;
         const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
         const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null);
-        const lastFrame = Cast(targetDoc.lastFrame, 'number', null);
-        const curFrame = NumCast(targetDoc._currentFrame);
         let prevSelected = this.itemIndex;
         // Functionality for group with up
         let didZoom = activeItem.presMovement;
         for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) {
             didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom;
         }
-        if (lastFrame !== undefined && curFrame >= 1) {
-            // Case 1: There are still other frames and should go through all frames before going to previous slide
-            this.prevKeyframe(targetDoc, activeItem);
-        } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
+        if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
             // Case 2: There are no other frames so it should go to the previous slide
             prevSelected = Math.max(0, prevSelected - 1);
             this.nextSlide(prevSelected);
             this.rootDoc._itemIndex = prevSelected;
-            if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame);
         } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
             // Case 3: Pres loop is on so it should go to the last slide
             this.gotoDocument(this.childDocs.length - 1, activeItem);
@@ -313,14 +295,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
-            let focusSpeed = 500;
             if (activeItem.presActiveFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
                 const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
                 if (context) {
-                    const contextView = DocumentManager.Instance.getFirstDocumentView(context);
-                    if (contextView?.ComponentView) {
-                        CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime);
+                    const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
+                    if (ffview) {
+                        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
                         context._currentFrame = NumCast(activeItem.presActiveFrame);
                     }
                 }
@@ -335,9 +316,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
                 this.startTempMedia(targetDoc, activeItem);
             }
-            if (targetDoc?.lastFrame !== undefined) {
-                targetDoc._currentFrame = 0;
-            }
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
             this.turnOffEdit();
@@ -345,14 +323,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
-        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
-        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
-        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
-        const clippable = [DocumentType.COMPARISON].includes(target.type as any);
-        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(target.type as any) && target.activeFrame === undefined;
-        const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
-        const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
+    static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
+        const targetType = target?.type as any;
+        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
+        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
+        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
+        const clippable = [DocumentType.COMPARISON].includes(targetType);
+        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(targetType) && target?.activeFrame === undefined;
+        const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
+        const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const dataannos = false;
         return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
@@ -360,8 +339,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     playAnnotation = (anno: AudioField) => {};
     @action
-    static restoreTargetDocView(bestTarget: Doc, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTarget)) {
-        const presTransitionTime = `all ${transTime}ms`;
+    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTargetView?.rootDoc)) {
+        if (!bestTargetView) return;
+        const bestTarget = bestTargetView.rootDoc;
         let changed = false;
         if (pinProps?.pinDocLayout) {
             if (
@@ -420,7 +400,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
                 .forEach(data => {
                     const doc = DocServer.GetCachedRefField(data.id) as Doc;
-                    doc._dataTransition = presTransitionTime;
+                    doc._dataTransition = `all ${transTime}ms`;
                     doc.x = data.x;
                     doc.y = data.y;
                     doc._width = data.w;
@@ -456,8 +436,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
         }
         if (changed) {
-            bestTarget._viewTransition = presTransitionTime;
-            return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
+            return bestTargetView.setViewTransition('all', transTime);
         }
     }
 
@@ -519,8 +498,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
         }
     }
-
-    static _navTimer: NodeJS.Timeout | undefined;
     /**
      * This method makes sure that cursor navigates to the element that
      * has the option open and last in the group.
@@ -549,55 +526,59 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const selViewCache = Array.from(this.selectedArray);
         const dragViewCache = Array.from(this._dragArray);
         const eleViewCache = Array.from(this._eleArray);
-        const self = this;
         const resetSelection = action(() => {
-            const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc);
+            const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
             if (presDocView) SelectionManager.SelectView(presDocView, false);
-            self.rootDoc.presStatus = presStatus;
-            self.clearSelectedArray();
-            selViewCache.forEach(doc => self.addToSelectedArray(doc));
-            self._dragArray.splice(0, self._dragArray.length, ...dragViewCache);
-            self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
+            this.rootDoc.presStatus = presStatus;
+            this.clearSelectedArray();
+            selViewCache.forEach(doc => this.addToSelectedArray(doc));
+            this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
         });
-        const openInTab = (doc: Doc, finished?: () => void) => {
-            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.add);
+        const createDocView = (doc: Doc, finished?: () => void) => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
+            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox);
             this.layoutDoc.presCollection = targetDoc;
-            // this still needs some fixing
-            setTimeout(resetSelection, 500);
-            if (doc !== targetDoc) {
-                setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context
-            } else {
-                finished?.();
-            }
         };
-        PresBox.NavigateToTarget(targetDoc, activeItem, openInTab, srcContext, includesDoc || tab ? undefined : resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? undefined : resetSelection);
     };
 
-    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
+    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) {
+        if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
+            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
+            return;
+        }
+        const restoreLayout = () => {
+            // After navigating to the document, if it is added as a presPinView then it will
+            // adjust the pan and scale to that of the pinView when it was added.
+            const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
+            if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
+                // targetDoc may or may not be displayed.  so get the first available document (or alias) view that matches targetDoc and use it
+                PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+            }
+        };
         // If openDocument is selected then it should open the document for the user
         if (activeItem.openDocument) {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
-        } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
-            LightboxView.SetLightboxDoc(undefined);
-            const options: DocFocusOptions = {
-                willZoom: activeItem.presMovement !== PresMovement.Pan,
-                zoomScale: NumCast(activeItem.presZoom, 1),
-                zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
-                noSelect: true,
-                originatingDoc: activeItem,
-            };
-            DocumentManager.Instance.jumpToDocument(targetDoc, options, openInTab, srcContext ? [srcContext] : [], finished);
-        } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
-            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
-        }
-        // After navigating to the document, if it is added as a presPinView then it will
-        // adjust the pan and scale to that of the pinView when it was added.
-        const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
-        if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
-            PresBox._navTimer && clearTimeout(PresBox._navTimer);
-            // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
-            const bestTargetView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
-            if (bestTargetView?.props.Document) PresBox._navTimer = PresBox.restoreTargetDocView(bestTargetView?.props.Document, { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+            setTimeout(restoreLayout);
+        } else {
+            if (targetDoc && activeItem.presMovement !== PresMovement.None) {
+                LightboxView.SetLightboxDoc(undefined);
+                const options: DocFocusOptions = {
+                    willZoom: activeItem.presMovement !== PresMovement.Pan,
+                    zoomScale: NumCast(activeItem.presZoom, 1),
+                    zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                    noSelect: true,
+                    originatingDoc: activeItem,
+                };
+
+                var containerDocContext = srcContext ? [srcContext] : [];
+                while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
+                    containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                }
+
+                DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished);
+            }
+            restoreLayout();
         }
     }
 
@@ -1824,32 +1805,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return undefined;
     };
 
-    // Case in which the document has keyframes to navigate to next key frame
-    @action
-    nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => {
-        const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
-        const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            tagDoc._currentFrame = 0;
-            // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
-            // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
-        tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
-        tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
-    };
-
-    @action
-    prevKeyframe = (tagDoc: Doc, actItem: Doc): void => {
-        const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
-        const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            tagDoc._currentFrame = 0;
-            // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice());
-        tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
-    };
+    _keyTimer: NodeJS.Timeout | undefined;
 
     /**
      * Returns the collection type as a string for headers
@@ -2022,76 +1978,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
-    @action
-    getList = (list: any): List<number> => list;
-
-    @action
-    updateList = (list: any): List<number> => {
-        const targetDoc: Doc = this.targetDoc;
-        const x: List<number> = list;
-        x[x.length - 1] = NumCast(targetDoc._scrollY);
-        return x;
-    };
-
-    @action
-    newFrame = () => {
-        const activeItem: Doc = this.activeItem;
-        const type: string = StrCast(this.targetDoc.type);
-        if (!activeItem.frameList) activeItem.frameList = new List<number>();
-        switch (type) {
-            case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
-                this.updateList(activeItem.frameList);
-                break;
-        }
-    };
-
-    @computed get frameListHeader() {
-        return (
-            <div className="frameList-header">
-                &nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null}
-                <div className={'frameList-headerButtons'}>
-                    <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}>
-                        <div
-                            className={'headerButton'}
-                            onClick={e => {
-                                e.stopPropagation();
-                                this.newFrame();
-                            }}>
-                            <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} />
-                        </div>
-                    </Tooltip>
-                    <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}>
-                        <div className={'headerButton'} onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
-                        </div>
-                    </Tooltip>
-                </div>
-            </div>
-        );
-    }
-
-    @computed get frameList() {
-        const frameList: List<number> = this.getList(this.activeItem.frameList);
-        return !frameList ? null : (
-            <div className="frameList-container">
-                {frameList.map(value => (
-                    <div className="framList-item" />
-                ))}
-            </div>
-        );
-    }
-
-    @computed get playButtonFrames() {
-        const targetDoc = this.targetDoc;
-        return !this.targetDoc ? null : (
-            <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
-                <div>{NumCast(targetDoc._currentFrame)}</div>
-                <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
-                <div>{NumCast(targetDoc.lastFrame)}</div>
-            </div>
-        );
-    }
-
     @computed get playButtons() {
         const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
         const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
@@ -2143,7 +2029,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </Tooltip>
                 <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
                     Slide {this.itemIndex + 1} / {this.childDocs.length}
-                    {this.playButtonFrames}
                 </div>
                 <div className="presPanel-divider"></div>
                 {this.props.PanelWidth() > 250 ? (
@@ -2202,9 +2087,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         clearTimeout(this._presTimer);
     };
 
-    @action
-    startMarqueeCreateSlide = () => (PresBox.startMarquee = true);
-
     AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
         var indexNum = 0;
         for (let i = 0; i < index.length; i++) {
@@ -2271,7 +2153,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     </Tooltip>
                     <div className="presPanel-button-text">
                         Slide {this.itemIndex + 1} / {this.childDocs.length}
-                        {this.playButtonFrames}
                     </div>
                     <div className="presPanel-divider" />
                     <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
-- 
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/PDFBox.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

