From 7c1dd332dc444d2d34cc9cc142de5998ca4622b5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 4 Feb 2021 14:32:38 -0500
Subject: fixed scrolling animations in view bounds for webbox and pdfBox's
 particualrly for lightbox views.   added a docViewPath prop for docuentViews.

---
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx    | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 6c7512f7c..e25a46a5d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -904,13 +904,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
         SelectionManager.DeselectAll();
         if (this.props.Document.scrollHeight) {
-            const annotOn = Cast(doc.annotationOn, Doc) as Doc;
+            // only consider the document to be an annotation if it's an annotation on this collection's document (ignore annotations on some other document that are somehow being focused on here)
+            const annotOn = Doc.AreProtosEqual(doc.annotationOn as Doc, this.props.Document) ? Cast(doc.annotationOn, Doc) as Doc : undefined;
             let delay = 1000;
             if (!annotOn) {
                 !dontCenter && this.props.focus(doc);
                 afterFocus && setTimeout(afterFocus, delay);
             } else {
-                const contextHgt = NumCast(annotOn._height);
+                const contextHgt = this.props.PanelHeight();
                 const curScroll = NumCast(this.props.Document._scrollTop);
                 let scrollTo = curScroll;
                 if (curScroll + contextHgt < NumCast(doc.y)) {
@@ -922,7 +923,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     this.props.Document._scrollPreviewY = this.props.Document._scrollY = scrollTo;
                     delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0;
                     !dontCenter && this.props.focus(this.props.Document);
-                    afterFocus && setTimeout(afterFocus, delay);
+                    afterFocus && setTimeout(() => afterFocus?.(delay ? true : false), delay);
                 } else {
                     !dontCenter && delay && this.props.focus(this.props.Document);
                     afterFocus?.(!dontCenter && delay ? true : false);
@@ -988,6 +989,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             pinToPres: this.props.pinToPres,
             whenActiveChanged: this.props.whenActiveChanged,
             parentActive: this.parentActive,
+            docViewPath: this.props.docViewPath,
             DataDoc: childData,
             Document: childLayout,
             ContainingCollectionView: this.props.CollectionView,
@@ -1005,6 +1007,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             searchFilterDocs: this.searchFilterDocs,
             focus: this.focusDocument,
             styleProvider: this.getClusterColor,
+            layerProvider: this.props.layerProvider,
             freezeDimensions: this.props.childFreezeDimensions,
             dropAction: StrCast(this.props.Document.childDropAction) as dropActionType,
             bringToFront: this.bringToFront,
-- 
cgit v1.2.3-70-g09d2


From 0e5891eab7f53697b764b7e9da5163db0351a0a2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 Feb 2021 19:25:48 -0500
Subject: overhaul of link anchors on text boxes to use actual Documents to
 represent selected text.  Also got rid of _scrollY and _scrollPreviewY so
 that all document regions can be focused on using focus() and the new
 scrollFocus() mechanisim

---
 src/client/documents/DocumentTypes.ts              |   4 +-
 src/client/documents/Documents.ts                  |   7 +
 src/client/util/DocumentManager.ts                 |   8 +-
 src/client/views/GestureOverlay.tsx                |  17 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/StyleProvider.tsx                 |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  14 +-
 src/client/views/nodes/DocumentView.tsx            |  14 +-
 src/client/views/nodes/LinkDocPreview.tsx          |  15 +-
 src/client/views/nodes/PDFBox.tsx                  |   5 +-
 src/client/views/nodes/PresBox.tsx                 |   2 +-
 src/client/views/nodes/WebBox.tsx                  | 184 ++++++++----------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 207 ++++++++++-----------
 .../formattedText/FormattedTextBoxComment.tsx      |  18 +-
 .../views/nodes/formattedText/RichTextMenu.tsx     |  24 +--
 src/client/views/nodes/formattedText/marks_rts.ts  |  20 +-
 src/client/views/pdf/PDFViewer.tsx                 | 116 +++++-------
 src/fields/documentSchemas.ts                      |   7 +-
 src/fields/util.ts                                 |   4 +-
 20 files changed, 306 insertions(+), 372 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 37a148e55..080657fd8 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -38,5 +38,7 @@ export enum DocumentType {
 
     LINKDB = "linkdb",          // database of links  ??? why do we have this
     SCRIPTDB = "scriptdb",      // database of scripts
-    GROUPDB = "groupdb"         // database of groups
+    GROUPDB = "groupdb",        // database of groups
+
+    TEXTANCHOR = "textanchor"   // selection of text in a text box
 }
\ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1a4aae17e..7d6db06d5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -373,6 +373,9 @@ export namespace Docs {
             }],
             [DocumentType.GROUP, {
                 layout: { view: EmptyBox, dataField: defaultDataKey }
+            }],
+            [DocumentType.TEXTANCHOR, {
+                layout: { view: EmptyBox, dataField: defaultDataKey }
             }]
         ]);
 
@@ -785,6 +788,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.DOCHOLDER), document, { title: document ? document.title + "" : "container", targetDropAction: "move", ...options });
         }
 
+        export function TextanchorDocument(options: DocumentOptions = {}) {
+            return InstanceFromProto(Prototypes.get(DocumentType.TEXTANCHOR), document, { targetDropAction: "move", ...options });
+        }
+
         export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Freeform }, id);
         }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 7af16ed6e..f5f6b6f67 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -149,7 +149,8 @@ export class DocumentManager {
         const highlight = () => {
             const finalDocView = getFirstDocView(targetDoc);
             if (finalDocView) {
-                finalDocView.layoutDoc.scrollToLinkID = linkDoc?.[Id];
+                const parent = targetDoc?.annotationOn as Doc;
+                if (parent) finalDocView.layoutDoc.scrollToAnchorID = targetDoc?.[Id];
                 Doc.linkFollowHighlight(finalDocView.props.Document);
             }
         };
@@ -159,7 +160,7 @@ export class DocumentManager {
             const first = getFirstDocView(annotatedDoc);
             if (first) {
                 annotatedDoc = first.props.Document;
-                first.props.focus(annotatedDoc, false);
+                first.focus(targetDoc, false);
             }
         }
         if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
@@ -190,7 +191,6 @@ export class DocumentManager {
                 highlight();
             } else {  // otherwise try to get a view of the context of the target
                 const targetDocContextView = getFirstDocView(targetDocContext);
-                targetDocContext._scrollY = targetDocContext._scrollPreviewY = NumCast(targetDocContext._scrollTop, 0);  // this will force PDFs to activate and load their annotations / allow scrolling
                 if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first..
                     targetDocContext._viewTransition = "transform 500ms";
                     targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
@@ -208,7 +208,7 @@ export class DocumentManager {
                             } else if (delay > 1500) {
                                 // 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.props.Document);
-                                if (targetDoc.layout) {
+                                if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document
                                     Doc.SetInPlace(targetDoc, "annotationOn", undefined, false);
                                     createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
                                 }
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 76cb0112e..9306cf9ae 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -516,7 +516,7 @@ export class GestureOverlay extends Touchable {
     }
 
     handleLineGesture = (): boolean => {
-        let actionPerformed = false;
+        const actionPerformed = false;
         const B = this.svgBounds;
 
         // get the two targets at the ends of the line
@@ -525,21 +525,6 @@ export class GestureOverlay extends Touchable {
         const target1 = document.elementFromPoint(ep1.X, ep1.Y);
         const target2 = document.elementFromPoint(ep2.X, ep2.Y);
 
-        // callback function to be called by each target
-        const callback = (doc: Doc) => {
-            if (!this._d1) {
-                this._d1 = doc;
-            }
-            // we don't want to create a link of both endpoints are the same document (doing so makes drawing an l very hard)
-            else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
-                // we don't want to create a link between ink strokes (doing so makes drawing a t very hard)
-                if (this._d1.type !== "ink" && doc.type !== "ink") {
-                    DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link", "");
-                    actionPerformed = true;
-                }
-            }
-        };
-
         const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
             {
                 bubbles: true,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 44aa41f85..bc3d05005 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -101,7 +101,7 @@ export class MainView extends React.Component {
         }
         new InkStrokeProperties();
         this._sidebarContent.proto = undefined;
-        DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's
+        DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's
 
         DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
 
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3956b8c5b..058d21c92 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -12,7 +12,7 @@ import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
 import { CollectionViewType } from './collections/CollectionView';
 import { MainView } from './MainView';
-import { DocumentViewProps } from "./nodes/DocumentView";
+import { DocumentViewProps, DocumentView } from "./nodes/DocumentView";
 import { FieldViewProps } from './nodes/FieldView';
 import "./StyleProvider.scss";
 import React = require("react");
@@ -111,6 +111,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps |
                 case DocumentType.LABEL: docColor = docColor || (doc.annotationOn !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break;
                 case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break;
                 case DocumentType.LINK: return "transparent";
+                case DocumentType.WEB:
+                case DocumentType.PDF:
                 case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break;
                 case DocumentType.COL:
                     if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e25a46a5d..a52522def 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -271,12 +271,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     @undoBatch
     @action
-    internalPdfAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) {
+    internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) {
         const dragDoc = annoDragData.dropDocument!;
         const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)];
         dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]);
         dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]);
-        annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
         this.bringToFront(dragDoc);
         return true;
     }
@@ -303,7 +302,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
         const [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
         if (this.isAnnotationOverlay !== true && de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp);
-        if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp);
+        if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData, xp, yp);
         if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, xp, yp);
         return false;
     }
@@ -920,7 +919,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                     scrollTo = Math.max(0, NumCast(doc.y) - 50);
                 }
                 if (curScroll !== scrollTo || this.props.Document._viewTransition) {
-                    this.props.Document._scrollPreviewY = this.props.Document._scrollY = scrollTo;
                     delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0;
                     !dontCenter && this.props.focus(this.props.Document);
                     afterFocus && setTimeout(() => afterFocus?.(delay ? true : false), delay);
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 2cac2d0b8..defa4dbf0 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -58,8 +58,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                             // however, the dropped document isn't so accessible.  What we do is set the newly created link document on the documentView
                             // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it.
                             dropEv.linkDragData?.linkDropCallback?.(dropEv as { linkDocument: Doc }); // bcz: typescript can't figure out that this is valid even though we tested dropEv.linkDocument above
-                            runInAction(() => this.props.View.LinkBeingCreated = dropEv.linkDocument);
-                            setTimeout(action(() => this.props.View.LinkBeingCreated = undefined), 0);
                         }
                         linkDrag?.end();
                     },
@@ -118,7 +116,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     DocumentLinksButton.AnnotationId = undefined;
                 } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) {
                     const sourceDoc = DocumentLinksButton.StartLink;
-                    const targetDoc = this.props.View.props.Document;
+                    const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document;
                     const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, "long drag");
 
                     LinkManager.currentLink = linkDoc;
@@ -163,15 +161,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
             endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink;
             startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink;
             const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag", undefined, undefined, true);
-            // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks ().  see note above in onLInkButtonMoved
-            if (endLinkView) {
-                endLinkView.LinkBeingCreated = linkDoc;
-                DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView.LinkBeingCreated = linkDoc);
-                setTimeout(action(() => {
-                    DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView.LinkBeingCreated = undefined);
-                    endLinkView.LinkBeingCreated = undefined;
-                }), 0);
-            }
+
             LinkManager.currentLink = linkDoc;
 
             if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2f56f5b00..463e59bd1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -40,12 +40,15 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { PresBox } from './PresBox';
 import { RadialMenu } from './RadialMenu';
 import React = require("react");
+import { LinkDocPreview } from "./LinkDocPreview";
+import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
 
 export type DocAfterFocusFunc = (notFocused: boolean) => boolean;
 export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => void;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any;
 export interface DocComponentView {
     getAnchor: () => Doc;
+    scrollFocus?: (doc: Doc, smooth: boolean) => void;
     back?: () => boolean;
     forward?: () => boolean;
     url?: () => string;
@@ -373,6 +376,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     }
 
+    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => {
+        this._componentView?.scrollFocus?.(doc, !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc); // bcz: smooth parameter should really be passed into focus() instead of inferred here
+        return this.props.focus(doc, willZoom, scale, afterFocus, dontCenter, focused);
+    }
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 &&
             (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
@@ -710,7 +717,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
     contentScaling = () => this.ContentScale;
     onClickFunc = () => this.onClickHandler;
-    makeLink = () => this.props.DocumentView.LinkBeingCreated; // pass the link placeholde to child views so they can react to make a specialized anchor.  This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined.
     setContentView = (view: { getAnchor: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view;
     @observable contentsActive: () => boolean = returnFalse;
     @action setContentsActive = (setActive: () => boolean) => this.contentsActive = setActive;
@@ -729,9 +735,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 contentsActive={this.setContentsActive}
                 parentActive={this.parentActive}
                 ScreenToLocalTransform={this.screenToLocal}
-                makeLink={this.makeLink}
                 rootSelected={this.rootSelected}
                 onClick={this.onClickFunc}
+                focus={this.focus}
                 layoutKey={this.finalLayoutKey} />
             {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors}
             {this.hideLinkButton ? (null) :
@@ -865,7 +871,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
     public ContentRef = React.createRef<HTMLDivElement>();
 
-    @observable LinkBeingCreated: Opt<Doc>;  // see DocumentLinksButton for explanation of how this works
     @observable public docView: DocumentViewInternal | undefined | null;
 
     get Document() { return this.props.Document; }
@@ -907,6 +912,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
     contentsActive = () => this.docView?.contentsActive();
+    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => {
+        return this.docView?.focus(doc, willZoom, scale, afterFocus, dontCenter, focused);
+    }
     getBounds = () => {
         if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
             return undefined;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 75ca6059e..8051568ff 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -26,6 +26,7 @@ export class LinkDocPreview extends React.Component<Props> {
     @observable public static LinkInfo: Opt<{ linkDoc?: Doc; linkSrc: Doc; href?: string; Location: number[], docprops: DocumentViewSharedProps }>;
     @observable _targetDoc: Opt<Doc>;
     @observable _toolTipText = "";
+    _linkTarget: Opt<Doc>;
     _editRef = React.createRef<HTMLDivElement>();
 
     @action
@@ -58,17 +59,13 @@ export class LinkDocPreview extends React.Component<Props> {
                 runInAction(() => this._toolTipText = "external => " + this.props.href);
             }
         } else if (linkDoc && linkSrc) {
-            const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), linkSrc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
-            const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
+            const anchor1 = linkDoc.anchor1 as Doc;
+            const anchor2 = linkDoc.anchor2 as Doc;
+            this._linkTarget = Doc.AreProtosEqual(anchor1, linkSrc) || Doc.AreProtosEqual(anchor1.annotationOn as Doc, linkSrc) ? anchor2 : anchor1;
+            const target = this._linkTarget?.annotationOn ? await DocCastAsync(this._linkTarget.annotationOn) : this._linkTarget;
             runInAction(() => {
                 this._toolTipText = "";
                 LinkDocPreview.TargetDoc = this._targetDoc = target;
-                if (this._targetDoc) {
-                    this._targetDoc._scrollToPreviewLinkID = linkDoc?.[Id];
-                    if (anchor !== this._targetDoc && anchor) {
-                        this._targetDoc._scrollPreviewY = NumCast(anchor?.y);
-                    }
-                }
             });
         }
     }
@@ -89,7 +86,7 @@ export class LinkDocPreview extends React.Component<Props> {
                 </div>
             </div>
             :
-            <DocumentView
+            <DocumentView ref={r => this._linkTarget !== this._targetDoc && this._linkTarget && r?.focus(this._linkTarget)}
                 Document={this._targetDoc}
                 moveDocument={returnFalse}
                 rootSelected={returnFalse}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index ec9a75302..496caedaa 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -79,8 +79,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
         }
     }
 
+    scrollFocus = (doc: Doc, smooth: boolean) => doc !== this.rootDoc && this._pdfViewer?.scrollFocus(doc, smooth);
+    getAnchor = () => this.rootDoc;
     componentWillUnmount() { this._selectReactionDisposer?.(); }
     componentDidMount() {
+        this.props.setContentView?.(this);
         this._selectReactionDisposer = reaction(() => this.props.isSelected(),
             () => {
                 document.removeEventListener("keydown", this.onKeyDown);
@@ -218,7 +221,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
     static pdfpromise = new Map<string, Pdfjs.PDFPromise<Pdfjs.PDFDocumentProxy>>();
     render() {
         TraceMobx();
-        if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0) || this.props.Document._scrollY !== undefined) {
+        if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0)) {
             this._displayPdfLive = true;
         }
         if (this._displayPdfLive) {
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index b6feace12..589a1c2ae 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -364,7 +364,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
         bestTarget && runInAction(() => {
             if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) {
                 bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
-                bestTarget._scrollY = activeItem.presPinViewScroll;
+                bestTarget._scrollTop = activeItem.presPinViewScroll;
             } else if (bestTarget.type === DocumentType.COMPARISON) {
                 bestTarget._clipWidth = activeItem.presPinClipWidth;
             } else if (bestTarget.type === DocumentType.VID) {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 1dfe88f78..ee152ddb3 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from "mobx-react";
 import { Dictionary } from "typescript-collections";
 import * as WebRequest from 'web-request';
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym, Opt } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
 import { Id } from "../../../fields/FieldSymbols";
 import { HtmlField } from "../../../fields/HtmlField";
@@ -24,11 +24,11 @@ import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
+import { MarqueeAnnotator } from "../MarqueeAnnotator";
 import { Annotation } from "../pdf/Annotation";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./WebBox.scss";
 import React = require("react");
-import { MarqueeAnnotator } from "../MarqueeAnnotator";
 const htmlToText = require("html-to-text");
 
 type WebDocument = makeInterface<[typeof documentSchema]>;
@@ -44,15 +44,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _iframeIndicatorRef = React.createRef<HTMLDivElement>();
     private _iframeDragRef = React.createRef<HTMLDivElement>();
+    private _ignoreScroll = false;
+    private _initialScroll: Opt<number>;
     @observable private _marqueeing: number[] | undefined;
     @observable private _url: string = "hello";
     @observable private _pressX: number = 0;
     @observable private _pressY: number = 0;
     @observable private _iframe: HTMLIFrameElement | null = null;
     @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
-
-    get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; }
-    set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; }
     get scrollHeight() { return this.webpage?.scrollHeight || 1000; }
     get webpage() { return this._iframe?.contentDocument?.children[0]; }
 
@@ -65,9 +64,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         this._annotationKey = this._annotationKey + "-" + this.urlHash(this._url);
     }
 
-    iframeLoaded = action((e: any) => {
+    iframeLoaded = (e: any) => {
         const iframe = this._iframe;
         if (iframe?.contentDocument) {
+            if (this._initialScroll !== undefined && this._outerRef.current && this.webpage) {
+                this.webpage.scrollTop = this._initialScroll;
+                this._outerRef.current.scrollTop = this._initialScroll;
+                this._initialScroll = undefined;
+            }
             iframe.setAttribute("enable-annotation", "true");
             iframe.contentDocument.addEventListener("click", undoBatch(action(e => {
                 let href = "";
@@ -76,81 +80,53 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                 }
                 if (href) {
                     this.submitURL(href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin));
+                    if (this.webpage) {
+                        this.webpage.scrollTop = NumCast(this.layoutDoc._scrollTop);
+                        this.webpage.scrollLeft = 0;
+                    }
                 }
             })));
             iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false);
-            if (this.webpage) {
-                this.webpage.scrollTop = NumCast(this.layoutDoc._scrollTop);
-                this.webpage.scrollLeft = NumCast(this.layoutDoc._scrollLeft);
-            }
-        }
-        this._disposers.scrollReaction?.();
-        this._disposers.scrollReaction = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }),
-            ({ scrollY, scrollX }) => {
-                const delay = this._outerRef.current ? 0 : 250; // wait for mainCont and try again to scroll
-                const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/);
-                const duration = durationStr ? Number(durationStr[1]) : 1000;
-                if (scrollY !== undefined) {
-                    this._forceSmoothScrollUpdate = true;
-                    setTimeout(() => this.layoutDoc._scrollY = undefined, duration);
-                    setTimeout(() => this.webpage && smoothScroll(duration, this.webpage as any as HTMLElement, Math.abs(scrollY || 0)), delay);
-                    setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollY || 0), () => this.layoutDoc._scrollTop = scrollY), delay);
-                }
-                if (scrollX !== undefined) {
-                    this._forceSmoothScrollUpdate = true;
-                    setTimeout(() => this.layoutDoc._scrollX = undefined, duration);
-                    setTimeout(() => this.webpage && smoothScroll(duration, this.webpage as any as HTMLElement, Math.abs(scrollX || 0)), delay);
-                    setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollX || 0), () => this.layoutDoc._scrollLeft = scrollX), delay);
-                }
-            },
-            { fireImmediately: true }
-        );
-        this._disposers.scrollTop = reaction(() => this.layoutDoc._scrollTop,
-            scrollTop => {
-                const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/);
-                const duration = durationStr ? Number(durationStr[1]) : 1000;
-                if (scrollTop !== this._outerRef.current?.scrollTop && scrollTop !== undefined && this._forceSmoothScrollUpdate) {
-                    this.webpage!.scrollTop = scrollTop;
-                    this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollTop || 0), () => this._forceSmoothScrollUpdate = true);
-                } else this._forceSmoothScrollUpdate = true;
-            },
-            { fireImmediately: true }
-        );
-    });
-    _forceSmoothScrollUpdate = true;
-
-    updateScroll = (x: Opt<number>, y: Opt<number>) => {
-        if (y !== undefined) {
-            this._outerRef.current!.scrollTop = y;
-            this.layoutDoc._scrollY = undefined;
-        }
-        if (x !== undefined) {
-            this._outerRef.current!.scrollLeft = x;
-            this.layoutDoc.scrollX = undefined;
         }
     }
 
-    iframeWheel = (e: any) => {
-        if (this._forceSmoothScrollUpdate && e.target?.children) {
-            this.webpage && setTimeout(action(() => {
-                this.webpage!.scrollLeft = 0;
-                const scrollTop = this.webpage!.scrollTop;
-                const scrollLeft = this.webpage!.scrollLeft;
-                this._outerRef.current!.scrollTop = scrollTop;
-                this._outerRef.current!.scrollLeft = scrollLeft;
-                if (this.layoutDoc._scrollTop !== scrollTop) {
-                    this._forceSmoothScrollUpdate = false;
-                    this.layoutDoc._scrollTop = scrollTop;
-                }
-                if (this.layoutDoc._scrollLeft !== scrollLeft) {
-                    this._forceSmoothScrollUpdate = false;
-                    this.layoutDoc._scrollLeft = scrollLeft;
-                }
-            }));
+    @action
+    onWheelScroll = (scrollTop: number) => {
+        if (this.webpage && this._outerRef.current) {
+            this.webpage.scrollLeft = 0;
+            this._outerRef.current.scrollTop = scrollTop;
+            this._outerRef.current.scrollLeft = 0;
+            this._ignoreScroll = true;
+            if (this.layoutDoc._scrollTop !== scrollTop) {
+                this.layoutDoc._scrollTop = scrollTop;
+            }
+            this._ignoreScroll = false;
         }
     }
+    iframeWheel = (e: any) => this.webpage && e.target?.children && this.onWheelScroll(this.webpage.scrollTop);
+    onWheel = (e: React.WheelEvent) => {
+        this._outerRef.current && this.onWheelScroll(this._outerRef.current.scrollTop);
+        e.stopPropagation();
+    }
 
     getAnchor = () => this.rootDoc;
+    scrollFocus = (doc: Doc, smooth: boolean) => {
+        if (doc !== this.rootDoc && this.webpage && this._outerRef.current) {
+            this._initialScroll !== undefined && (this._initialScroll = NumCast(doc.y));
+            this._ignoreScroll = true;
+            if (smooth) {
+                smoothScroll(500, this.webpage as any as HTMLElement, NumCast(doc.y));
+                smoothScroll(500, this._outerRef.current, NumCast(doc.y));
+            } else {
+                this.webpage.scrollTop = NumCast(doc.y);
+                this._outerRef.current.scrollTop = NumCast(doc.y);
+            }
+            smooth && (this.layoutDoc._scrollTop = NumCast(doc.y));
+            this._ignoreScroll = false;
+        } else {
+            this._initialScroll = NumCast(doc.y);
+        }
+    }
 
     async componentDidMount() {
         this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
@@ -158,9 +134,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField);
         runInAction(() => this._url = urlField?.url.toString() || "");
 
-        this._disposers.scrollMove = reaction(() => this.layoutDoc.x || this.layoutDoc.y,
-            () => this.updateScroll(this.layoutDoc._scrollLeft, this.layoutDoc._scrollTop));
-
         this._disposers.selection = reaction(() => this.props.isSelected(),
             selected => {
                 if (!selected) {
@@ -190,6 +163,30 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                 this.dataDoc.text = htmlToText.fromString(result.content);
             }
         }
+
+        var quickScroll: string | undefined = "";
+        this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop),
+            (scrollTop) => {
+                if (quickScroll !== undefined) {
+                    this._initialScroll = scrollTop;
+                }
+                else if (!this._ignoreScroll && this._outerRef.current && this.webpage) {
+                    const viewTrans = StrCast(this.Document._viewTransition);
+                    const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
+                    const durationSecStr = viewTrans.match(/([0-9.]*)s/);
+                    const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
+                    if (duration) {
+                        smoothScroll(duration, this.webpage as any as HTMLElement, scrollTop);
+                        smoothScroll(duration, this._outerRef.current, scrollTop);
+                    } else {
+                        this.webpage.scrollTop = scrollTop;
+                        this._outerRef.current.scrollTop = scrollTop;
+                    }
+                }
+            },
+            { fireImmediately: true }
+        );
+        quickScroll = undefined;
     }
 
     componentWillUnmount() {
@@ -353,22 +350,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             }
         }, 1500);
     }
-
     onLongPressMove = (e: PointerEvent) => {
         // this._pressX = e.clientX;
         // this._pressY = e.clientY;
     }
-
     onLongPressUp = (e: PointerEvent) => {
-        if (this._longPressSecondsHack) {
-            clearTimeout(this._longPressSecondsHack);
-        }
-        if (this._iframeIndicatorRef.current) {
-            this._iframeIndicatorRef.current.classList.remove("active");
-        }
-        if (this._iframeDragRef.current) {
-            while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild);
-        }
+        this._longPressSecondsHack && clearTimeout(this._longPressSecondsHack);
+        this._iframeIndicatorRef.current?.classList.remove("active");
+        while (this._iframeDragRef.current?.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild);
     }
 
     specificContextMenu = (e: React.MouseEvent): void => {
@@ -377,7 +366,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" });
         funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : Doc.NativeWidth(this.layoutDoc), icon: "snowflake" });
         cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
-
     }
 
     @computed
@@ -389,7 +377,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
         } else if (field instanceof WebField) {
             const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href;
             //    view = <iframe className="webBox-iframe" src={url} onLoad={e => { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }}
-
             view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded}
                 // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
                 // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
@@ -402,10 +389,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
 
     @computed
     get content() {
-        const view = this.urlContent;
         const frozen = !this.props.isSelected() || DocumentDecorations.Instance?.Interacting;
         const scale = this.props.scaling?.() || 1;
-
         return (<>
             <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")}
                 style={{
@@ -414,7 +399,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                     transform: `scale(${scale})`
                 }}
                 onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
-                {view}
+                {this.urlContent}
             </div>
             {!frozen ? (null) :
                 <div className="webBox-overlay" style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? undefined : "all" }}
@@ -429,7 +414,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
 
     @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); }
     @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); }
-
     @computed get annotationLayer() {
         TraceMobx();
         return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
@@ -452,7 +436,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
 
     panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
     panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
-    scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop));
+    scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     render() {
         const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false;
         const scale = this.props.scaling?.() || 1;
@@ -467,21 +451,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
                         width: `${100 / scale}%`, height: `${100 / scale}%`, transform: `scale(${scale})`,
                         pointerEvents: this.layoutDoc.isAnnotating && !inactiveLayer ? "all" : "none"
                     }}
-                    onWheel={e => {
-                        const target = this._outerRef.current;
-                        if (this._forceSmoothScrollUpdate && target && this.webpage) {
-                            setTimeout(action(() => {
-                                target.scrollLeft = 0;
-                                const scrollTop = target.scrollTop;
-                                const scrollLeft = target.scrollLeft;
-                                this.webpage!.scrollTop = scrollTop;
-                                this.webpage!.scrollLeft = scrollLeft;
-                                if (this.layoutDoc._scrollTop !== scrollTop) this.layoutDoc._scrollTop = scrollTop;
-                                if (this.layoutDoc._scrollLeft !== scrollLeft) this.layoutDoc._scrollLeft = scrollLeft;
-                            }));
-                        }
-                        e.stopPropagation();
-                    }}
+                    onWheel={this.onWheel}
                     onPointerDown={this.onMarqueeDown}
                     onScroll={e => e.stopPropagation()}
                 >
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index b3abcf90a..2409f36a0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -92,6 +92,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     public static Instance: FormattedTextBox;
     public ProseRef?: HTMLDivElement;
     public get EditorView() { return this._editorView; }
+    public get SidebarKey() { return this.fieldKey + "-sidebar"; }
     private _boxRef: React.RefObject<HTMLDivElement> = React.createRef();
     private _ref: React.RefObject<HTMLDivElement> = React.createRef();
     private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -104,15 +105,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _recordingStart: number = 0;
     private _pause: boolean = false;
-    private _animatingScroll: number = 0; // hack to prevent scroll values from being written to document when scroll is animating
+    private _ignoreScroll = false;
 
     @computed get _recording() { return this.dataDoc?.audioState === "recording"; }
     set _recording(value) {
         this.dataDoc.audioState = value ? "recording" : undefined;
     }
 
-    @observable private _entered = false;
-
     public static FocusedBox: FormattedTextBox | undefined;
     public static SelectOnLoad = "";
     public static PasteOnLoad: ClipboardEvent | undefined;
@@ -164,29 +163,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one. 
     // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing.
     public RemoveLinkFromDoc(linkDoc?: Doc) {
+        this.unhighlightSearchTerms();
         const state = this._editorView?.state;
-        if (state && linkDoc && this._editorView) {
-            var allLinks: any[] = [];
+        const a1 = linkDoc?.anchor1 as Doc;
+        const a2 = linkDoc?.anchor2 as Doc;
+        if (state && a1 && a2 && this._editorView) {
+            this.removeDocument(a1);
+            this.removeDocument(a2);
+            var allFoundLinkAnchors: any[] = [];
             state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => {
-                const foundMark = findLinkMark(node.marks);
-                const newHrefs = foundMark?.attrs.allLinks.filter((a: any) => a.href.includes(linkDoc[Id])) || [];
-                allLinks = newHrefs.length ? newHrefs : allLinks;
+                const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || [];
+                allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors;
                 return true;
             });
-            if (allLinks.length) {
-                this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allLinks }));
+            if (allFoundLinkAnchors.length) {
+                this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors }));
             }
         }
     }
-    // removes all the specified link referneces from the selection. 
+    // removes all the specified link references from the selection. 
     // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references.
-    public RemoveLinkFromSelection(allLinks: { href: string, title: string, linkId: string, targetId: string }[]) {
+    public RemoveAnchorFromSelection(allAnchors: { href: string, title: string, linkId: string, targetId: string }[]) {
         const state = this._editorView?.state;
         if (state && this._editorView) {
-            this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allLinks }));
+            this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
         }
     }
 
+    getAnchor = () => this.makeLinkAnchor(undefined, "add:right", undefined, "Anchored Selection");
+
     linkOnDeselect: Map<string, string> = new Map();
 
     doLinkOnDeselect() {
@@ -234,11 +239,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
 
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, () => this.rootDoc, targetCreator), e.pageX, e.pageY, {
                 dragComplete: e => {
+                    const anchor = this.makeLinkAnchor(undefined, "add:right", undefined, "a link");
                     if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) {
-                        e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note");
+                        e.linkDocument = DocUtils.MakeLink({ doc: anchor }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note");
                         e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.rootDoc;
                     }
-                    e.linkDocument && e.annoDragData?.dropDocument && this.makeLinkToSelection(e.linkDocument[Id], "a link", "add:right", e.annoDragData.dropDocument[Id]);
                     e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument
                 }
             });
@@ -270,8 +275,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     this.linkOnDeselect.set(key, value);
 
                     const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
-                    const allLinks = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }];
-                    const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "add:right", title: value });
+                    const allAnchors = [{ href: Utils.prepend("/doc/" + id), title: value, anchorId: id }];
+                    const link = this._editorView.state.schema.marks.linkAnchor.create({ allAnchors, location: "add:right", title: value });
                     const mval = this._editorView.state.schema.marks.metadataVal.create();
                     const offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
                     tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
@@ -407,9 +412,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             res.map(r => r.map(h => flattened.push(h)));
             const lastSel = Math.min(flattened.length - 1, this._searchIndex);
             this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
-            const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!;
-            const allLinks = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }];
-            const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, title: "a link", location });
+            const anchor = new Doc();
+            const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!;
+            const allAnchors = [{ href: Utils.prepend("/doc/" + anchor[Id]), title: "a link", anchorId: anchor[Id] }];
+            const link = this._editorView.state.schema.marks.linkAnchor.create({ allAnchors, title: "a link", location });
             this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link));
         }
     }
@@ -507,8 +513,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     }
     linkDrop = (data: { linkDocument: Doc }) => {
         const anchor1Title = data.linkDocument.anchor1 instanceof Doc ? StrCast(data.linkDocument.anchor1.title) : "-untitled-";
-        const anchor1Id = data.linkDocument.anchor1 instanceof Doc ? data.linkDocument.anchor1[Id] : "";
-        this.makeLinkToSelection(data.linkDocument[Id], anchor1Title, "add:right", anchor1Id);
+        const anchor1 = data.linkDocument.anchor1 instanceof Doc ? data.linkDocument.anchor1 : undefined;
+        this.makeLinkAnchor(anchor1, "add:right", undefined, anchor1Title);
     }
 
     getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
@@ -825,37 +831,46 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         };
     }
 
-    makeLinkToSelection(linkId: string, title: string, location: string, targetId: string, targetHref?: string) {
+    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) {
         const state = this._editorView?.state;
         if (state) {
-            const href = targetHref ?? Utils.prepend("/doc/" + linkId);
             const sel = state.selection;
             const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
             let tr = state.tr.addMark(sel.from, sel.to, splitter);
-            sel.from !== sel.to && tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
-                if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
-                    const allLinks = [{ href, title, targetId, linkId }];
-                    allLinks.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allLinks ?? []));
-                    const link = state.schema.marks.linkAnchor.create({ allLinks, title, location, linkId });
-                    tr = tr.addMark(pos, pos + node.nodeSize, link);
-                }
-            });
-            this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true;  // need to allow permissions for adding links to readonly/augment only documents
-            this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
-            this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
+            if (sel.from !== sel.to) {
+                const anchor = anchorDoc ?? Docs.Create.TextanchorDocument();
+                const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]);
+                if (anchor !== anchorDoc) this.addDocument(anchor);
+                tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
+                    if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
+                        const allAnchors = [{ href, title, anchorId: anchor[Id] }];
+                        allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? []));
+                        const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location });
+                        tr = tr.addMark(pos, pos + node.nodeSize, link);
+                    }
+                });
+                this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true;  // need to allow permissions for adding links to readonly/augment only documents
+                this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
+                this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
+                Doc.GetProto(anchor).title = this._editorView?.state.doc.textBetween(sel.from, sel.to);
+                return anchor;
+            }
+            return anchorDoc ?? this.rootDoc;
         }
+        return anchorDoc ?? this.rootDoc;
     }
 
     IsActive = () => {
         return this.active();//this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
     }
 
-    scrollToLinkId = (linkId: string) => {
-        const findLinkFrag = (frag: Fragment, editor: EditorView) => {
+    scrollFocus = (doc: Doc, smooth: boolean) => {
+        const anchorId = doc[Id];
+        const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
             const nodes: Node[] = [];
             let hadStart = start !== 0;
             frag.forEach((node, index) => {
-                const examinedNode = findLinkNode(node, editor);
+                const examinedNode = findAnchorNode(node, editor);
                 if (examinedNode?.node.textContent) {
                     nodes.push(examinedNode.node);
                     !hadStart && (start = index + examinedNode.start);
@@ -864,20 +879,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             });
             return { frag: Fragment.fromArray(nodes), start };
         };
-        const findLinkNode = (node: Node, editor: EditorView) => {
+        const findAnchorNode = (node: Node, editor: EditorView) => {
             if (!node.isText) {
-                const content = findLinkFrag(node.content, editor);
+                const content = findAnchorFrag(node.content, editor);
                 return { node: node.copy(content.frag), start: content.start };
             }
             const marks = [...node.marks];
             const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor);
-            return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => linkId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined;
+            return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => anchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined;
         };
 
         let start = 0;
-        if (this._editorView && linkId) {
+        if (this._editorView && anchorId) {
             const editor = this._editorView;
-            const ret = findLinkFrag(editor.state.doc.content, editor);
+            const ret = findAnchorFrag(editor.state.doc.content, editor);
 
             if (ret.frag.size > 2 && ret.start >= 0) {
                 let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
@@ -885,16 +900,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     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
                 }
                 editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
-                const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
-                setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
-                setTimeout(() => this.unhighlightSearchTerms(), 2000);
+                const escAnchorId = anchorId[0] > '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId;
+                addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" });
+                setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), 1500);
             }
-            Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
-            Doc.SetInPlace(this.layoutDoc, "_scrollToPreviewLinkID", undefined, false);
         }
     }
 
     componentDidMount() {
+        this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
         this.props.contentsActive?.(this.IsActive);
         this._cachedLinks = DocListCast(this.Document.links);
         this._disposers.sidebarheight = reaction(
@@ -916,27 +930,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 }
             }
         );
-        this._disposers.linkMaker = reaction(
-            () => this.props.makeLink?.(),
-            (linkDoc: Opt<Doc>) => {
-                if (linkDoc) {
-                    const a1 = Cast(linkDoc.anchor1, Doc, null);
-                    const a2 = Cast(linkDoc.anchor2, Doc, null);
-                    const otherAnchor = Doc.AreProtosEqual(a1, this.rootDoc) ? a2 : a1;
-                    const anchor2Title = StrCast(otherAnchor.title, "-untitled-");
-                    const anchor2Id = otherAnchor?.[Id] || "";
-                    this.makeLinkToSelection(linkDoc[Id], anchor2Title, "add:right", anchor2Id);
-                }
-            },
-            { fireImmediately: true }
-        );
         this._disposers.editorState = reaction(
             () => {
-                if (!this.dataDoc || !this.layoutDoc) return undefined;
-                if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey]) {
-                    return { data: Cast(this.dataDoc[this.props.fieldKey], RichTextField, null), str: StrCast(this.dataDoc[this.props.fieldKey]) };
-                }
-                return { data: Cast(this.layoutDoc[this.props.fieldKey], RichTextField, null), str: StrCast(this.layoutDoc[this.props.fieldKey]) };
+                const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined :
+                    this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey] ?
+                        this.dataDoc : this.layoutDoc;
+                return !whichDoc ? undefined : { data: Cast(whichDoc[this.props.fieldKey], RichTextField, null), str: StrCast(whichDoc[this.props.fieldKey]) };
             },
             incomingValue => {
                 if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1013,38 +1012,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 }
             );
         }
-        this._disposers.previewScrollToRegion = reaction(() => StrCast(this.layoutDoc._scrollToPreviewLinkID),
-            scrollToLinkID => this.props.renderDepth === -1 && scrollToLinkID && this.scrollToLinkId(scrollToLinkID), { fireImmediately: true }
-        );
-        this._disposers.scrollToRegion = reaction(() => StrCast(this.layoutDoc.scrollToLinkID),
-            scrollToLinkID => scrollToLinkID && this.scrollToLinkId(scrollToLinkID), { fireImmediately: true }
-        );
+        var quickScroll: string | undefined = "";
         this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop),
             pos => {
-                const durationMiliStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/);
-                const durationSecStr = StrCast(this.Document._viewTransition).match(/([0-9.]*)s/);
-                const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
-                if (duration) {
-                    this._animatingScroll++;
-                    this._scrollRef.current && smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0));
-                    setTimeout(() => this._animatingScroll--, duration);
-                } else {
-                    this._scrollRef.current?.scrollTo({ top: pos });
+                if (!this._ignoreScroll && this._scrollRef.current) {
+                    const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
+                    const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
+                    const durationSecStr = viewTrans.match(/([0-9.]*)s/);
+                    const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
+                    if (duration) {
+                        smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0));
+                    } else {
+                        this._scrollRef.current.scrollTo({ top: pos });
+                    }
                 }
             }, { fireImmediately: true }
         );
-        this._disposers.scrollY = reaction(() => Cast(this.layoutDoc._scrollY, "number", null),
-            pos => {
-                this.Document._scrollY = undefined;
-                pos !== undefined && setTimeout(() => this.layoutDoc._scrollTop = pos, this._scrollRef.current ? 0 : 250);
-            }, { fireImmediately: true }
-        );
-        this._disposers.scrollPreviewY = reaction(() => Cast(this.layoutDoc.scrollPreviewY, "number", null),
-            pos => {
-                this.Document.scrollPreviewY = undefined;
-                pos !== undefined && setTimeout(() => this.layoutDoc._scrollTop = pos, this._scrollRef.current ? 0 : 250);
-            }, { fireImmediately: true }
-        );
+        quickScroll = undefined;
 
         setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight)));
     }
@@ -1228,8 +1212,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 handleScrollToSelection: (editorView) => {
                     const docPos = editorView.coordsAtPos(editorView.state.selection.from);
                     const viewRect = self._ref.current!.getBoundingClientRect();
-                    if (docPos.top < viewRect.top || docPos.top > viewRect.bottom) {
-                        docPos && (self._scrollRef.current!.scrollTop += (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale);
+                    const scrollRef = self._scrollRef.current;
+                    if ((docPos.top < viewRect.top || docPos.top > viewRect.bottom) && scrollRef) {
+                        const scrollPos = scrollRef.scrollTop + (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale;
+                        if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
+                            scrollPos && smoothScroll(500, scrollRef, scrollPos);
+                        } else {
+                            scrollRef.scrollTo({ top: scrollPos });
+                        }
                     }
                     return true;
                 },
@@ -1423,6 +1413,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         }
     }
 
+    static _highlightStyleSheet: any = addStyleSheet();
     static _bulletStyleSheet: any = addStyleSheet();
     static _userStyleSheet: any = addStyleSheet();
     _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason.  This flags the 2nd click as happening on a selection which allows bullet points to toggle
@@ -1609,9 +1600,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema));
         eve.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
     }
-    onscrolled = (ev: React.UIEvent) => {
-        if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && !this._animatingScroll) {
-            this.layoutDoc._scrollTop = this._scrollRef.current!.scrollTop;
+    onScroll = (ev: React.UIEvent) => {
+        if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this._scrollRef.current) {
+            this._ignoreScroll = true;
+            this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop;
+            this._ignoreScroll = false;
         }
     }
 
@@ -1655,7 +1648,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     }
 
     @computed get sidebarHandle() {
-        const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.author).length;
+        const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
         return this.props.noSidebar || (!this.props.isSelected() && !(annotated && !this.sidebarWidth())) ? (null) :
             <div className="formattedTextBox-sidebar-handle"
                 style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: annotated ? "lightblue" : this.props.styleProvider?.(this.props.Document, this.props, StyleProp.WidgetColor) }}
@@ -1675,9 +1668,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
             xMargin: 0,
             yMargin: 0,
             chromeStatus: "enabled",
-            scaleField: this.annotationKey + "-scale",
+            scaleField: this.SidebarKey + "-scale",
             isAnnotationOverlay: true,
-            fieldKey: this.annotationKey,
+            fieldKey: this.SidebarKey,
             fitContentsToDoc: fitToBox,
             select: emptyFunction,
             active: this.annotationsActive,
@@ -1750,14 +1743,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     onPointerDown={this.onPointerDown}
                     onMouseUp={this.onMouseUp}
                     onWheel={this.onPointerWheel}
-                    onPointerEnter={action(() => this._entered = true)}
-                    onPointerLeave={action(e => {
-                        this._entered = false;
-                        const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
-                        for (let child: any = target; child; child = child?.parentElement) {
-                            child === this._ref.current! && (this._entered = true);
-                        }
-                    })}
                     onDoubleClick={this.onDoubleClick}
                 >
                     <div className={`formattedTextBox-outer${selected ? "-selected" : ""}`} ref={this._scrollRef}
@@ -1766,7 +1751,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                             pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined,
                             overflow: this.layoutDoc._singleLine ? "hidden" : undefined,
                         }}
-                        onScroll={this.onscrolled} onDrop={this.ondrop} >
+                        onScroll={this.onScroll} onDrop={this.ondrop} >
                         <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget}
                             style={{
                                 padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${padding}px`,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 9508f8034..05f0fefd8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -227,7 +227,7 @@ export class FormattedTextBoxComment {
             state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
             child = child || (nbef && state.selection.$from.nodeBefore);
             const mark = child ? findLinkMark(child.marks) : undefined;
-            const href = forceUrl || (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allLinks.find((item: { href: string }) => item.href)?.href;
+            const href = forceUrl || (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allAnchors.find((item: { href: string }) => item.href)?.href;
             if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) {
                 try {
                     ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
@@ -250,18 +250,16 @@ export class FormattedTextBoxComment {
                     FormattedTextBoxComment.tooltipText.style.overflow = "hidden";
                 }
                 if (href.indexOf(Utils.prepend("/doc/")) === 0) {
-                    const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                    const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     FormattedTextBoxComment.tooltipText.textContent = "target not found...";
                     (FormattedTextBoxComment.tooltipText as any).href = "";
-                    docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => {
-                        if (linkDoc instanceof Doc) {
+                    anchorDoc && DocServer.GetRefField(anchorDoc).then(async anchor => {
+                        if (anchor instanceof Doc) {
+                            const linkDoc = DocListCast(anchor.links)[0];
                             (FormattedTextBoxComment.tooltipText as any).href = href;
                             FormattedTextBoxComment.linkDoc = linkDoc;
-                            const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
-                            const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
-                            if (anchor !== target && anchor && target) {
-                                target._scrollPreviewY = NumCast(anchor?.y);
-                            }
+                            const targetanchor = LinkManager.getOppositeAnchor(linkDoc, anchor);
+                            const target = targetanchor?.annotationOn ? await DocCastAsync(targetanchor.annotationOn) : targetanchor;
                             if (target?.author) {
                                 FormattedTextBoxComment.showCommentbox("", view, nbef);
 
@@ -295,7 +293,7 @@ export class FormattedTextBoxComment {
                                         </div>
                                     </div>
                                     <div className="FormattedTextBoxComment-preview-wrapper">
-                                        <DocumentView
+                                        <DocumentView ref={(r) => targetanchor && target !== targetanchor && r?.focus(targetanchor)}
                                             Document={target}
                                             moveDocument={returnFalse}
                                             rootSelected={returnFalse}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index dc630af74..b041c4fc9 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -806,7 +806,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
                 <p>Linked to:</p>
                 <input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} />
                 <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "add:right")}>Apply hyperlink</button>
-                <div className="divider"></div>
+                <div className="divider" />
                 <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
             </div>;
 
@@ -819,7 +819,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
         const node = this.view.state.selection.$from.nodeAfter;
         const link = node && node.marks.find(m => m.type.name === "link");
         if (link) {
-            const href = link.attrs.allLinks.length > 0 ? link.attrs.allLinks[0].href : undefined;
+            const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined;
             if (href) {
                 if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                     const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
@@ -851,21 +851,21 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
     // TODO: should check for valid URL
     @undoBatch
     makeLinkToURL = (target: string, lcoation: string) => {
-        ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRadd:rightight", "", target);
+        ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, "onRadd:rightight", "", target);
     }
 
     @undoBatch
     @action
     deleteLink = () => {
         if (this.view) {
-            const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor);
-            if (link) {
-                const allLinks = link.attrs.allLinks.slice();
-                this.TextView.RemoveLinkFromSelection(link.attrs.allLinks);
+            const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor);
+            if (linkAnchor) {
+                const allAnchors = linkAnchor.attrs.allAnchors.slice();
+                this.TextView.RemoveAnchorFromSelection(allAnchors);
                 // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected.
-                allLinks.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => {
-                    const linkId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
-                    linkId && DocServer.GetRefField(linkId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
+                allAnchors.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => {
+                    const anchorId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                    anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
                 });
             }
         }
@@ -877,8 +877,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps>   {
         let startIndex = $start.index();
         let endIndex = $start.indexAfter();
 
-        while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) startIndex--;
-        while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) endIndex++;
+        while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--;
+        while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) endIndex++;
 
         let startPos = $start.start();
         let endPos = startPos;
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index ea239e4d3..aceba0387 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -22,7 +22,7 @@ export const marks: { [index: string]: MarkSpec } = {
     // element.
     linkAnchor: {
         attrs: {
-            allLinks: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] },
+            allAnchors: { default: [] as { href: string, title: string, anchorId: string }[] },
             showPreview: { default: true },
             location: { default: null },
             title: { default: null },
@@ -31,17 +31,23 @@ export const marks: { [index: string]: MarkSpec } = {
         inclusive: false,
         parseDOM: [{
             tag: "a[href]", getAttrs(dom: any) {
-                return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.dataset.targetids }], location: dom.getAttribute("location"), };
+                return {
+                    allAnchors: [{ href: dom.getAttribute("shref"), title: dom.getAttribute("title"), anchorId: dom.getAttribute("class") }],
+                    location: dom.getAttribute("location"),
+                };
             }
         }],
         toDOM(node: any) {
-            const targetids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, "");
-            const targethrefs = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.href, "");
-            const linkids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, "");
+            const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string, title: string, anchorId: string }) => p ? p + " " + item.href : item.href, "");
+            const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string, title: string, anchorId: string }) => p ? p + " " + item.anchorId : item.anchorId, "");
             return node.attrs.docref && node.attrs.title ?
-                ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
+                ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", {
+                    ...node.attrs,
+                    class: "prosemirror-attribution",
+                    href: node.attrs.allAnchors[0].href,
+                }, node.attrs.title], ["br"]] :
                 //node.attrs.allLinks.length === 1 ?
-                ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, "data-targethrefs": targethrefs, title: `${node.attrs.title}`, href: node.attrs.allLinks[0]?.href, style: `text-decoration: ${linkids === " " ? "underline" : undefined}` }, 0];
+                ["a", { ...node.attrs, class: anchorids, "data-targethrefs": targethrefs, title: node.attrs.title, href: node.attrs.allAnchors[0]?.href, style: `text-decoration: "underline"` }, 0];
             // ["div", { class: "prosemirror-anchor" },
             //     ["span", { class: "prosemirror-linkBtn" },
             //         ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0],
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index eadf58027..0477192d5 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -23,8 +23,6 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec
 import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { MarqueeAnnotator } from "../MarqueeAnnotator";
 import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBoxComment } from "../nodes/formattedText/FormattedTextBoxComment";
-import { LinkDocPreview } from "../nodes/LinkDocPreview";
 import { Annotation } from "./Annotation";
 import { AnchorMenu } from "./AnchorMenu";
 import "./PDFViewer.scss";
@@ -88,6 +86,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
     private _coverPath: any;
     private _lastSearch = false;
     private _viewerIsSetup = false;
+    private _ignoreScroll = false;
+    private _smoothScrolling = true;
 
     @computed get allAnnotations() {
         return DocUtils.FilterDocs(DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]), this.props.docFilters(), this.props.docRangeFilters(), undefined);
@@ -119,12 +119,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         }
         runInAction(() => this._showWaiting = true);
         this.props.startupLive && this.setupPdfJsViewer();
-        if (this._mainCont.current) {
-            this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0;
-            const observer = new _global.ResizeObserver(action((entries: any) => this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0)));
-            observer.observe(this._mainCont.current);
-            this._mainCont.current.addEventListener("scroll", (e) => (e.target as any).scrollLeft = 0);
-        }
+        this._mainCont.current?.addEventListener("scroll", e => (e.target as any).scrollLeft = 0);
 
         this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.rootDoc),
             m => {
@@ -141,36 +136,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
                 (SelectionManager.Views().length === 1) && this.setupPdfJsViewer();
             },
             { fireImmediately: true });
-        this._disposers.scrollY = reaction(
-            () => this.Document._scrollY,
-            (scrollY) => {
-                if (scrollY !== undefined) {
-                    (this._showCover || this._showWaiting) && this.setupPdfJsViewer();
-                    if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
-                        const delay = this._mainCont.current ? 0 : 250; // wait for mainCont and try again to scroll
-                        const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/);
-                        const duration = durationStr ? Number(durationStr[1]) : 1000;
-                        setTimeout(() => this.Document._scrollY = undefined, duration + delay);
-                        setTimeout(() => this._mainCont.current && smoothScroll(duration, this._mainCont.current, Math.abs(scrollY || 0), () => this.layoutDoc._scrollTop = scrollY), delay);
-                    }
-                }
-            },
-            { fireImmediately: true }
-        );
-        this._disposers.scrollPreviewY = reaction(
-            () => Cast(this.Document._scrollPreviewY, "number", null),
-            (scrollY) => {
-                if (scrollY !== undefined) {
-                    (this._showCover || this._showWaiting) && this.setupPdfJsViewer();
-                    if (this.props.renderDepth === -1 && scrollY >= 0) {
-                        if (!this._mainCont.current) setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0));
-                        else smoothScroll(1000, this._mainCont.current, scrollY || 0);
-                        this.Document._scrollPreviewY = undefined;
-                    }
-                }
-            },
-            { fireImmediately: true }
-        );
         this._disposers.curPage = reaction(
             () => this.Document._curPage,
             (page) => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page),
@@ -212,6 +177,19 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         }
     }
 
+    // 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, smooth: boolean) => {
+        const mainCont = this._mainCont.current;
+        if (mainCont) {
+            if (smooth) {
+                smoothScroll(500, mainCont, NumCast(doc.y));
+            } else {
+                mainCont.scrollTop = NumCast(doc.y);
+            }
+        }
+    }
+
     @action
     setupPdfJsViewer = async () => {
         if (this._viewerIsSetup) return;
@@ -220,14 +198,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         this.props.setPdfViewer(this);
         await this.initialLoad();
 
-        this._disposers.scrollTop = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null),
-            (stop) => {
-                if (stop !== undefined && this.layoutDoc._scrollY === undefined && this._mainCont.current) {
-                    (this._mainCont.current.scrollTop = stop);
-                }
-            },
-            { fireImmediately: true });
-
         this._disposers.filterScript = reaction(
             () => Cast(this.Document.filterScript, ScriptField),
             action(scriptField => {
@@ -242,13 +212,40 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         this.createPdfViewer();
     }
 
-    pagesinit = action(() => {
+    pagesinit = () => {
         if (this._pdfViewer._setDocumentViewerElement.offsetParent) {
-            this._pdfViewer.currentScaleValue = this._zoomed = 1;
+            runInAction(() => this._pdfViewer.currentScaleValue = this._zoomed = 1);
             this.gotoPage(this.Document._curPage || 1);
         }
         document.removeEventListener("pagesinit", this.pagesinit);
-    });
+        var quickScroll: string | undefined = "";
+        this._disposers.scroll = reaction(
+            () => NumCast(this.Document._scrollTop),
+            (pos) => {
+                if (!this._ignoreScroll) {
+                    (this._showCover || this._showWaiting) && this.setupPdfJsViewer();
+                    const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
+                    const delay = this._mainCont.current ? 0 : 250; // wait for mainCont and try again to scroll
+                    const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
+                    const durationSecStr = viewTrans.match(/([0-9.]*)s/);
+                    const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
+                    if (duration) {
+                        this._smoothScrolling = true;
+                        setTimeout(() => {
+                            this._mainCont.current && smoothScroll(duration, this._mainCont.current, Math.abs(pos || 0));
+                            setTimeout(() => this._smoothScrolling = false, duration);
+                        }, delay);
+                    } else {
+                        this._smoothScrolling = true;
+                        this._mainCont.current?.scrollTo({ top: Math.abs(pos || 0) });
+                        this._smoothScrolling = false;
+                    }
+                }
+            },
+            { fireImmediately: true }
+        );
+        quickScroll = undefined;
+    }
 
     createPdfViewer() {
         if (!this._mainCont.current) { // bcz: I don't think this is ever triggered or needed
@@ -301,30 +298,19 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
         }
     }
 
-    @action
-    scrollToFrame = (duration: number, top: number) => {
-        this._mainCont.current && smoothScroll(duration, this._mainCont.current, top);
-    }
-
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
         if (scrollToAnnotation) {
-            const offset = (this.props.PanelHeight() / this.contentScaling) / 2;
-            this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset);
+            this.scrollFocus(scrollToAnnotation, true);
             Doc.linkFollowHighlight(scrollToAnnotation);
         }
     }
 
-    pageDelay: any;
-    @action
     onScroll = (e: React.UIEvent<HTMLElement>) => {
-        if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) {
-            this.pageDelay && clearTimeout(this.pageDelay);
-            this.pageDelay = setTimeout(() => {
-                this.Document._scrollY === undefined && this._mainCont.current && (this.layoutDoc._scrollTop = this._mainCont.current.scrollTop);
-                this.pageDelay = undefined;
-                //this._pdfViewer && (this.Document._curPage = this._pdfViewer.currentPageNumber);
-            }, 1000);
+        if (this._mainCont.current && !this._smoothScrolling) {
+            this._ignoreScroll = true;
+            this.layoutDoc._scrollTop = this._mainCont.current.scrollTop;
+            this._ignoreScroll = false;
         }
     }
 
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index b10c2b015..f35c85110 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -26,10 +26,7 @@ export const documentSchema = createSchema({
     y: "number",                // y coordinate when in a freeform view 
     z: "number",                // z "coordinate" - non-zero specifies the overlay layer of a freeformview
     zIndex: "number",           // zIndex of a document in a freeform view
-    _scrollY: "number",          // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
-    _scrollX: "number",          // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
-    _scrollTop: "number",        // scroll position of a scrollable document (pdf, text, web)
-    _scrollLeft: "number",        // scroll position of a scrollable document (pdf, text, web)
+    _scrollTop: "number",       // scroll position of a scrollable document (pdf, text, web)
 
     // appearance properties on the layout
     "_backgroundGrid-spacing": "number", // the size of the grid for collection views
@@ -117,7 +114,7 @@ export const collectionSchema = createSchema({
     childLayoutTemplate: Doc, // layout template to use to render children of a collecion
     childLayoutString: "string", //layout string to use to render children of a collection
     childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template)
-    dontRegisterChildViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links scrollToLinkID: "string",   // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
+    dontRegisterChildViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links scrollToAnchorID: "string",   // id of anchor being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToAnchorID should be set to undefined by this doc after it sets up its scroll,etc.
     onChildClick: ScriptField, // script to run for each child when its clicked
     onChildDoubleClick: ScriptField, // script to run for each child when its clicked
     onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view
diff --git a/src/fields/util.ts b/src/fields/util.ts
index ecb3fb343..b9c5a13c1 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
 import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite } from "./Doc";
 import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
@@ -96,7 +96,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
         } else {
             DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
         }
-        !receiver[UpdatingFromServer] && UndoManager.AddEvent({
+        (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({
             redo: () => receiver[prop] = value,
             undo: () => receiver[prop] = curValue
         });
-- 
cgit v1.2.3-70-g09d2


From aed4a386bf57ba7b1b144bacd39f9f9ccabe0dfd Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 10 Feb 2021 22:32:29 -0500
Subject: simplified focus'ing on documents.  refactored scrollFocus code. 
 changed focus in 2D to move doc into view but not center.

---
 src/Utils.ts                                       |  7 +++
 src/client/util/DocumentManager.ts                 | 11 ++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 44 ++++++-------------
 src/client/views/nodes/DocumentView.tsx            | 18 ++++----
 src/client/views/nodes/PDFBox.tsx                  |  3 +-
 src/client/views/nodes/WebBox.tsx                  | 50 ++++++++++++----------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 10 ++++-
 src/client/views/pdf/PDFViewer.tsx                 | 15 ++++---
 8 files changed, 80 insertions(+), 78 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index c7074c3da..061c74611 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -191,6 +191,13 @@ export namespace Utils {
         return { h: h, s: s, l: l };
     }
 
+    export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number) {
+        if (scrollTop + contextHgt < targetY + Math.max(targetHgt * 1.1, 100)) {
+            return Math.min(scrollTop, targetY + Math.max(targetHgt * 1.1, 100) - contextHgt);
+        } else if (scrollTop > targetY - targetHgt * .1) {
+            return Math.max(0, targetY - targetHgt * .1);
+        }
+    }
 
     export function clamp(n: number, lower: number, upper: number) {
         return Math.max(lower, Math.min(upper, n));
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 9a8b662e7..ac7710d34 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -150,29 +150,26 @@ export class DocumentManager {
         };
         const docView = getFirstDocView(targetDoc, originatingDoc);
         let annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
-        if (annotatedDoc && annotatedDoc !== originatingDoc?.context && !targetDoc?.isPushpin) {
+        if (!docView && annotatedDoc && annotatedDoc !== originatingDoc?.context && targetDoc.type === DocumentType.TEXTANCHOR) {
             const first = getFirstDocView(annotatedDoc);
             if (first) {
                 annotatedDoc = first.rootDoc;
                 first.focus(targetDoc, false);
             }
-        }
-        if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
-            const sameContext = annotatedDoc && annotatedDoc === originatingDoc?.context;
+        } else if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
             if (originatingDoc?.isPushpin) {
                 docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => {
                     if (!didFocus || docView.rootDoc.hidden) {
                         docView.rootDoc.hidden = !docView.rootDoc.hidden;
                     }
                     return focusAndFinish();
-                }, sameContext, false);// don't want to focus the container if the source and target are in the same container, so pass 'sameContext' for dontCenter parameter
-                //finished?.();
+                });
             }
             else {
                 docView.select(false);
                 docView.rootDoc.hidden && (docView.rootDoc.hidden = undefined);
                 // @ts-ignore
-                docView.props.focus(docView.rootDoc, willZoom, undefined, focusAndFinish, sameContext, false);
+                docView.props.focus(docView.rootDoc, willZoom, undefined, focusAndFinish);
             }
             highlight();
         } else {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a52522def..68a65dfe7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -885,7 +885,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
     }
 
-    focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, didFocus?: boolean) => {
+    focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => {
         const state = HistoryUtil.getState();
 
         // TODO This technically isn't correct if type !== "doc", as
@@ -903,31 +903,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
         SelectionManager.DeselectAll();
         if (this.props.Document.scrollHeight) {
-            // only consider the document to be an annotation if it's an annotation on this collection's document (ignore annotations on some other document that are somehow being focused on here)
-            const annotOn = Doc.AreProtosEqual(doc.annotationOn as Doc, this.props.Document) ? Cast(doc.annotationOn, Doc) as Doc : undefined;
-            let delay = 1000;
-            if (!annotOn) {
-                !dontCenter && this.props.focus(doc);
-                afterFocus && setTimeout(afterFocus, delay);
-            } else {
-                const contextHgt = this.props.PanelHeight();
-                const curScroll = NumCast(this.props.Document._scrollTop);
-                let scrollTo = curScroll;
-                if (curScroll + contextHgt < NumCast(doc.y)) {
-                    scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 100) - contextHgt;
-                } else if (curScroll > NumCast(doc.y)) {
-                    scrollTo = Math.max(0, NumCast(doc.y) - 50);
-                }
-                if (curScroll !== scrollTo || this.props.Document._viewTransition) {
-                    delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0;
-                    !dontCenter && this.props.focus(this.props.Document);
-                    afterFocus && setTimeout(() => afterFocus?.(delay ? true : false), delay);
-                } else {
-                    !dontCenter && delay && this.props.focus(this.props.Document);
-                    afterFocus?.(!dontCenter && delay ? true : false);
-                }
-            }
-
+            this.props.focus(doc, undefined, undefined, afterFocus);
         } else {
             const layoutdoc = Doc.Layout(doc);
             const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
@@ -937,8 +913,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             let newPanY = savedState.py;
             if (!layoutdoc.annotationOn) {   // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
                 willZoom && this.setScaleToZoom(layoutdoc, scale);
-                newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeWidth(this.props.Document)) / 2 / this.zoomScaling() : 0);
-                newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeHeight(this.props.Document)) / 2 / this.zoomScaling() : 0);
+                const cx = NumCast(this.props.Document._panX);
+                const cy = NumCast(this.props.Document._panY);
+                const pwid = this.props.PanelWidth() / NumCast(this.props.Document._viewScale, 1);
+                const phgt = this.props.PanelHeight() / NumCast(this.props.Document._viewScale, 1);
+                const screen = { left: cx - pwid / 2, right: cx + pwid / 2, top: cy - phgt / 2, bot: cy + phgt / 2 };
+                const bounds = { left: NumCast(doc.x) - pwid / 10, right: NumCast(doc.x) + doc[WidthSym]() + pwid / 10, top: NumCast(doc.y) - phgt / 10, bot: NumCast(doc.y) + doc[HeightSym]() + phgt / 10 };
+                newPanX = cx + Math.min(0, bounds.left - screen.left) + Math.max(0, bounds.right - screen.right);
+                newPanY = cy + Math.min(0, bounds.top - screen.top) + Math.max(0, bounds.bot - screen.bot);
                 newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
                 HistoryUtil.pushState(newState);
             }
@@ -949,12 +931,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             }
             Doc.BrushDoc(this.props.Document);
 
-            const newDidFocus = didFocus || (newPanX !== savedState.px || newPanY !== savedState.py);
-
             const newAfterFocus = (didFocus: boolean) => {
                 afterFocus && setTimeout(() => {
                     // @ts-ignore
-                    if (afterFocus?.(!dontCenter && (didFocus || (newPanX !== savedState.px || newPanY !== savedState.py)))) {
+                    if (afterFocus?.(didFocus || (newPanX !== savedState.px || newPanY !== savedState.py))) {
                         this.Document._panX = savedState.px;
                         this.Document._panY = savedState.py;
                         this.Document[this.scaleFieldKey] = savedState.s;
@@ -964,7 +944,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 }, newPanX !== savedState.px || newPanY !== savedState.py ? 500 : 0);
                 return false;
             };
-            this.props.focus(this.props.Document, undefined, undefined, newAfterFocus, undefined, newDidFocus);
+            this.props.focus(this.props.Document, undefined, undefined, newAfterFocus);
             !doc.hidden && Doc.linkFollowHighlight(doc);
         }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 47668d0f3..a5698e138 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast } from "../../../fields/Doc";
+import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, HeightSym } from "../../../fields/Doc";
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -44,11 +44,11 @@ import { LinkDocPreview } from "./LinkDocPreview";
 import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
 
 export type DocAfterFocusFunc = (notFocused: boolean) => boolean;
-export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => void;
+export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => void;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any;
 export interface DocComponentView {
     getAnchor: () => Doc;
-    scrollFocus?: (doc: Doc, smooth: boolean) => void;
+    scrollFocus?: (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => void;
     back?: () => boolean;
     forward?: () => boolean;
     url?: () => string;
@@ -376,9 +376,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     }
 
-    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => {
-        this._componentView?.scrollFocus?.(doc, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
-        return this.props.focus(doc, willZoom, scale, afterFocus, dontCenter, focused);
+    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => {
+        if (this._componentView?.scrollFocus) {
+            return this._componentView?.scrollFocus?.(doc, !LinkDocPreview.LinkInfo, afterFocus); // bcz: smooth parameter should really be passed into focus() instead of inferred here
+        }
+        return this.props.focus(doc, willZoom, scale, afterFocus);
     }
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 &&
@@ -917,8 +919,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
     contentsActive = () => this.docView?.contentsActive();
-    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => {
-        return this.docView?.focus(doc, willZoom, scale, afterFocus, dontCenter, focused);
+    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, focused?: boolean) => {
+        return this.docView?.focus(doc, willZoom, scale, afterFocus, focused);
     }
     getBounds = () => {
         if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 496caedaa..4b926bb6f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -22,6 +22,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
+import { DocAfterFocusFunc } from './DocumentView';
 
 type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@@ -79,7 +80,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
         }
     }
 
-    scrollFocus = (doc: Doc, smooth: boolean) => doc !== this.rootDoc && this._pdfViewer?.scrollFocus(doc, smooth);
+    scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => { this._pdfViewer?.scrollFocus(doc, smooth, afterFocus); }
     getAnchor = () => this.rootDoc;
     componentWillUnmount() { this._selectReactionDisposer?.(); }
     componentDidMount() {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index ee152ddb3..d3d58b2f8 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -29,6 +29,7 @@ import { Annotation } from "../pdf/Annotation";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./WebBox.scss";
 import React = require("react");
+import { DocFocusFunc, DocAfterFocusFunc, DocumentView } from "./DocumentView";
 const htmlToText = require("html-to-text");
 
 type WebDocument = makeInterface<[typeof documentSchema]>;
@@ -110,22 +111,21 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
     }
 
     getAnchor = () => this.rootDoc;
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => {
         if (doc !== this.rootDoc && this.webpage && this._outerRef.current) {
-            this._initialScroll !== undefined && (this._initialScroll = NumCast(doc.y));
-            this._ignoreScroll = true;
-            if (smooth) {
-                smoothScroll(500, this.webpage as any as HTMLElement, NumCast(doc.y));
-                smoothScroll(500, this._outerRef.current, NumCast(doc.y));
-            } else {
-                this.webpage.scrollTop = NumCast(doc.y);
-                this._outerRef.current.scrollTop = NumCast(doc.y);
+            const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1));
+            if (scrollTo !== undefined) {
+                this._initialScroll !== undefined && (this._initialScroll = scrollTo);
+                this._ignoreScroll = true;
+                this.goTo(scrollTo, smooth ? 500 : 0);
+                this.layoutDoc._scrollTop = scrollTo;
+                this._ignoreScroll = false;
+                return afterFocus?.(true);
             }
-            smooth && (this.layoutDoc._scrollTop = NumCast(doc.y));
-            this._ignoreScroll = false;
         } else {
             this._initialScroll = NumCast(doc.y);
         }
+        afterFocus?.(false);
     }
 
     async componentDidMount() {
@@ -164,29 +164,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
             }
         }
 
-        var quickScroll: string | undefined = "";
+        var quickScroll = true;
         this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop),
             (scrollTop) => {
-                if (quickScroll !== undefined) {
+                if (quickScroll) {
                     this._initialScroll = scrollTop;
                 }
-                else if (!this._ignoreScroll && this._outerRef.current && this.webpage) {
+                else if (!this._ignoreScroll) {
                     const viewTrans = StrCast(this.Document._viewTransition);
                     const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
                     const durationSecStr = viewTrans.match(/([0-9.]*)s/);
                     const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
-                    if (duration) {
-                        smoothScroll(duration, this.webpage as any as HTMLElement, scrollTop);
-                        smoothScroll(duration, this._outerRef.current, scrollTop);
-                    } else {
-                        this.webpage.scrollTop = scrollTop;
-                        this._outerRef.current.scrollTop = scrollTop;
-                    }
+                    this.goTo(scrollTop, duration);
                 }
             },
             { fireImmediately: true }
         );
-        quickScroll = undefined;
+        quickScroll = false;
+    }
+
+    goTo = (scrollTop: number, duration: number) => {
+        if (this._outerRef.current && this.webpage) {
+            if (duration) {
+                smoothScroll(duration, this.webpage as any as HTMLElement, scrollTop);
+                smoothScroll(duration, this._outerRef.current, scrollTop);
+            } else {
+                this.webpage.scrollTop = scrollTop;
+                this._outerRef.current.scrollTop = scrollTop;
+            }
+        }
     }
 
     componentWillUnmount() {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c15c30803..183719e31 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -67,6 +67,7 @@ import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { LightboxView } from '../../LightboxView';
+import { DocAfterFocusFunc } from '../DocumentView';
 const translateGoogleApi = require("translate-google-api");
 
 export interface FormattedTextBoxProps {
@@ -867,7 +868,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
         return this.active();//this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
     }
 
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => {
         const anchorId = doc[Id];
         const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
             const nodes: Node[] = [];
@@ -905,8 +906,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                 editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
                 const escAnchorId = anchorId[0] > '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId;
                 addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" });
-                setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), 1500);
+                setTimeout(() => {
+                    clearStyleSheetRules(FormattedTextBox._highlightStyleSheet);
+                    afterFocus?.(true);
+                }, 1500);
             }
+        } else {
+            afterFocus?.(false);
         }
     }
 
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 0477192d5..dd9dfa733 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -28,6 +28,7 @@ import { AnchorMenu } from "./AnchorMenu";
 import "./PDFViewer.scss";
 const pdfjs = require('pdfjs-dist/es5/build/pdf.js');
 import React = require("react");
+import { DocAfterFocusFunc } from "../nodes/DocumentView";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 const _global = (window /* browser */ || global /* node */) as any;
@@ -179,15 +180,17 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
 
     // 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, smooth: boolean) => {
+    scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => {
         const mainCont = this._mainCont.current;
-        if (mainCont) {
-            if (smooth) {
-                smoothScroll(500, mainCont, NumCast(doc.y));
-            } else {
-                mainCont.scrollTop = NumCast(doc.y);
+        if (doc !== this.rootDoc && mainCont) {
+            const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1));
+            if (scrollTo !== undefined) {
+                if (smooth) smoothScroll(500, mainCont, scrollTo);
+                else mainCont.scrollTop = scrollTo;
+                return afterFocus?.(true);
             }
         }
+        afterFocus?.(false);
     }
 
     @action
-- 
cgit v1.2.3-70-g09d2


From c86cb3966ecb905c2dc6a6236b71d7ce9c8fb80f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Feb 2021 00:54:52 -0500
Subject: playing with trying to improve chaining of nested focus() method
 calls.

---
 src/Utils.ts                                       |  4 +-
 src/client/util/DocumentManager.ts                 | 51 +++++++++++---------
 src/client/views/collections/TabDocView.tsx        |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 56 ++++++++++++----------
 src/client/views/nodes/DocumentView.tsx            |  5 +-
 src/client/views/nodes/WebBox.tsx                  |  4 +-
 6 files changed, 67 insertions(+), 55 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 061c74611..770cef283 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -192,8 +192,8 @@ export namespace Utils {
     }
 
     export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number) {
-        if (scrollTop + contextHgt < targetY + Math.max(targetHgt * 1.1, 100)) {
-            return Math.min(scrollTop, targetY + Math.max(targetHgt * 1.1, 100) - contextHgt);
+        if (scrollTop + contextHgt < targetY + targetHgt * 1.1) {
+            return targetY + targetHgt * 1.1 - contextHgt;
         } else if (scrollTop > targetY - targetHgt * .1) {
             return Math.max(0, targetY - targetHgt * .1);
         }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ac7710d34..e5ef924d6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -143,45 +143,52 @@ export class DocumentManager {
         finished?: () => void,
     ): Promise<void> => {
         const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
-        const focusAndFinish = () => { finished?.(); return false; };
+        const docView = getFirstDocView(targetDoc, originatingDoc);
+        const focusAndFinish = (didFocus: boolean) => {
+            if (originatingDoc?.isPushpin) {
+                if (!didFocus || targetDoc.hidden) {
+                    targetDoc.hidden = !targetDoc.hidden;
+                }
+            } else {
+                targetDoc.hidden && (targetDoc.hidden = undefined);
+                docView?.select(false);
+            }
+            finished?.();
+            return false;
+        };
         const highlight = () => {
             const finalDocView = getFirstDocView(targetDoc);
             finalDocView && Doc.linkFollowHighlight(finalDocView.rootDoc);
         };
-        const docView = getFirstDocView(targetDoc, originatingDoc);
         let annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
+        const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
+        const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined;
+        const targetDocContext = contextDoc || annotatedDoc;
+        const targetDocContextView = targetDocContext && getFirstDocView(targetDocContext);
         if (!docView && annotatedDoc && annotatedDoc !== originatingDoc?.context && targetDoc.type === DocumentType.TEXTANCHOR) {
             const first = getFirstDocView(annotatedDoc);
             if (first) {
                 annotatedDoc = first.rootDoc;
                 first.focus(targetDoc, false);
             }
-        } else if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
-            if (originatingDoc?.isPushpin) {
-                docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => {
-                    if (!didFocus || docView.rootDoc.hidden) {
-                        docView.rootDoc.hidden = !docView.rootDoc.hidden;
-                    }
-                    return focusAndFinish();
-                });
-            }
-            else {
-                docView.select(false);
-                docView.rootDoc.hidden && (docView.rootDoc.hidden = undefined);
-                // @ts-ignore
-                docView.props.focus(docView.rootDoc, willZoom, undefined, focusAndFinish);
-            }
+        } else if (docView && (targetDocContextView || !targetDocContext)) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?      
+            const first = annotatedDoc && getFirstDocView(annotatedDoc);
+            const targetFocus = (outFocus?: boolean) => docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => focusAndFinish(outFocus || didFocus));
+            const annoFocus = (outFocus?: boolean) => first?.props.focus(first.rootDoc, false, undefined, (didFocus: boolean) => {
+                targetFocus(outFocus || didFocus);
+                return false;
+            });
+            const contextFocus = targetDocContext === annotatedDoc ? undefined : (outFocus?: boolean) => targetDocContextView?.props.focus(targetDocContext, false, undefined, (didFocus: boolean) => {
+                annoFocus(outFocus || didFocus);
+                return false;
+            });
+            (contextFocus || annoFocus || targetFocus)();
             highlight();
         } else {
-            const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
-            const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined;
-            const targetDocContext = contextDoc || annotatedDoc;
-
             if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
                 createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
                 highlight();
             } else {  // otherwise try to get a view of the context of the target
-                const targetDocContextView = getFirstDocView(targetDocContext);
                 if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first..
                     targetDocContext._viewTransition = "transform 500ms";
                     targetDocContextView.props.focus(targetDocContextView.rootDoc, willZoom);
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 38e1551e6..e1e1c8656 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -356,7 +356,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             </Tooltip>
         </>;
     }
-    focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, notFocused?: boolean) => {
+    focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => {
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
             this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 68a65dfe7..6fdd569da 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -907,53 +907,59 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         } else {
             const layoutdoc = Doc.Layout(doc);
             const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
-
             const newState = HistoryUtil.getState();
-            let newPanX = savedState.px;
-            let newPanY = savedState.py;
+            const { px, py } = layoutdoc.annotationOn ? savedState : willZoom ? this.setZoomToDoc(layoutdoc, scale) : this.setPanIntoView(doc);
             if (!layoutdoc.annotationOn) {   // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
-                willZoom && this.setScaleToZoom(layoutdoc, scale);
-                const cx = NumCast(this.props.Document._panX);
-                const cy = NumCast(this.props.Document._panY);
-                const pwid = this.props.PanelWidth() / NumCast(this.props.Document._viewScale, 1);
-                const phgt = this.props.PanelHeight() / NumCast(this.props.Document._viewScale, 1);
-                const screen = { left: cx - pwid / 2, right: cx + pwid / 2, top: cy - phgt / 2, bot: cy + phgt / 2 };
-                const bounds = { left: NumCast(doc.x) - pwid / 10, right: NumCast(doc.x) + doc[WidthSym]() + pwid / 10, top: NumCast(doc.y) - phgt / 10, bot: NumCast(doc.y) + doc[HeightSym]() + phgt / 10 };
-                newPanX = cx + Math.min(0, bounds.left - screen.left) + Math.max(0, bounds.right - screen.right);
-                newPanY = cy + Math.min(0, bounds.top - screen.top) + Math.max(0, bounds.bot - screen.bot);
-                newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
+                newState.initializers![this.Document[Id]] = { panX: px, panY: py };
                 HistoryUtil.pushState(newState);
             }
 
             if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) {
                 // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
-                if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", 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
+                if (!doc.z) this.setPan(px, py, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", 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
             }
             Doc.BrushDoc(this.props.Document);
 
-            const newAfterFocus = (didFocus: boolean) => {
-                afterFocus && setTimeout(() => {
-                    // @ts-ignore
-                    if (afterFocus?.(didFocus || (newPanX !== savedState.px || newPanY !== savedState.py))) {
+            !doc.hidden && Doc.linkFollowHighlight(doc);
+            this.props.focus(this.props.Document, undefined, undefined, (didFocus: boolean) => {
+                setTimeout(() => {
+                    if (afterFocus?.(didFocus || (px !== savedState.px || py !== savedState.py))) {
                         this.Document._panX = savedState.px;
                         this.Document._panY = savedState.py;
                         this.Document[this.scaleFieldKey] = savedState.s;
                         this.Document._viewTransition = savedState.pt;
                     }
                     doc.hidden && Doc.UnHighlightDoc(doc);
-                }, newPanX !== savedState.px || newPanY !== savedState.py ? 500 : 0);
+                }, px !== savedState.px || py !== savedState.py ? 500 : 0);
                 return false;
-            };
-            this.props.focus(this.props.Document, undefined, undefined, newAfterFocus);
-            !doc.hidden && Doc.linkFollowHighlight(doc);
+            });
         }
+    }
 
+    setPanIntoView = (doc: Doc) => {
+        const cx = NumCast(this.props.Document._panX);
+        const cy = NumCast(this.props.Document._panY);
+        const pwid = this.props.PanelWidth() / NumCast(this.props.Document._viewScale, 1);
+        const phgt = this.props.PanelHeight() / NumCast(this.props.Document._viewScale, 1);
+        const screen = { left: cx - pwid / 2, right: cx + pwid / 2, top: cy - phgt / 2, bot: cy + phgt / 2 };
+        const bounds = { left: NumCast(doc.x) - pwid / 10, right: NumCast(doc.x) + doc[WidthSym]() + pwid / 10, top: NumCast(doc.y) - phgt / 10, bot: NumCast(doc.y) + doc[HeightSym]() + phgt / 10 };
+        return {
+            px: cx + Math.min(0, bounds.left - screen.left) + Math.max(0, bounds.right - screen.right),
+            py: cy + Math.min(0, bounds.top - screen.top) + Math.max(0, bounds.bot - screen.bot)
+        };
     }
 
-    setScaleToZoom = (doc: Doc, scale: number = 0.75) => {
-        const pw = this.isAnnotationOverlay ? Doc.NativeWidth(this.props.Document) : this.props.PanelWidth();
-        const ph = this.isAnnotationOverlay ? Doc.NativeHeight(this.props.Document) : this.props.PanelHeight();
+    setZoomToDoc = (doc: Doc, scale: number = 0.75) => {
+        const pw = this.props.PanelWidth();
+        const ph = this.props.PanelHeight();
         pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height)));
+        const pwid = pw / NumCast(this.props.Document._viewScale, 1);
+        const phgt = ph / NumCast(this.props.Document._viewScale, 1);
+        const bounds = { left: NumCast(doc.x) - pwid / 10, right: NumCast(doc.x) + doc[WidthSym]() + pwid / 10, top: NumCast(doc.y) - phgt / 10, bot: NumCast(doc.y) + doc[HeightSym]() + phgt / 10 };
+        return {
+            px: (bounds.left + bounds.right) / 2,
+            py: (bounds.top + bounds.bot) / 2
+        };
     }
 
     onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a5698e138..d20866f94 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,7 +41,6 @@ import { PresBox } from './PresBox';
 import { RadialMenu } from './RadialMenu';
 import React = require("react");
 import { LinkDocPreview } from "./LinkDocPreview";
-import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment";
 
 export type DocAfterFocusFunc = (notFocused: boolean) => boolean;
 export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => void;
@@ -919,8 +918,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
     contentsActive = () => this.docView?.contentsActive();
-    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, focused?: boolean) => {
-        return this.docView?.focus(doc, willZoom, scale, afterFocus, focused);
+    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => {
+        return this.docView?.focus(doc, willZoom, scale, afterFocus);
     }
     getBounds = () => {
         if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d3d58b2f8..ef0df25b6 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from "mobx-react";
 import { Dictionary } from "typescript-collections";
 import * as WebRequest from 'web-request';
-import { Doc, DocListCast, HeightSym, WidthSym, Opt } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
 import { documentSchema } from "../../../fields/documentSchemas";
 import { Id } from "../../../fields/FieldSymbols";
 import { HtmlField } from "../../../fields/HtmlField";
@@ -26,10 +26,10 @@ import { ViewBoxAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { MarqueeAnnotator } from "../MarqueeAnnotator";
 import { Annotation } from "../pdf/Annotation";
+import { DocAfterFocusFunc } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./WebBox.scss";
 import React = require("react");
-import { DocFocusFunc, DocAfterFocusFunc, DocumentView } from "./DocumentView";
 const htmlToText = require("html-to-text");
 
 type WebDocument = makeInterface<[typeof documentSchema]>;
-- 
cgit v1.2.3-70-g09d2


From 42865450ea252e340d89a1f67c8a2dc0b9c6eae3 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Feb 2021 02:38:23 -0500
Subject: more tweaking to simplify link following mechanisms.

---
 src/client/util/DocumentManager.ts                 | 13 +---------
 src/client/util/LinkManager.ts                     |  6 ++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 29 ++++++++++++----------
 3 files changed, 20 insertions(+), 28 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index e5ef924d6..5d3f8af57 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -172,18 +172,7 @@ export class DocumentManager {
                 first.focus(targetDoc, false);
             }
         } else if (docView && (targetDocContextView || !targetDocContext)) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?      
-            const first = annotatedDoc && getFirstDocView(annotatedDoc);
-            const targetFocus = (outFocus?: boolean) => docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => focusAndFinish(outFocus || didFocus));
-            const annoFocus = (outFocus?: boolean) => first?.props.focus(first.rootDoc, false, undefined, (didFocus: boolean) => {
-                targetFocus(outFocus || didFocus);
-                return false;
-            });
-            const contextFocus = targetDocContext === annotatedDoc ? undefined : (outFocus?: boolean) => targetDocContextView?.props.focus(targetDocContext, false, undefined, (didFocus: boolean) => {
-                annoFocus(outFocus || didFocus);
-                return false;
-            });
-            (contextFocus || annoFocus || targetFocus)();
-            highlight();
+            docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => focusAndFinish(didFocus));
         } else {
             if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
                 createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 9034484d3..8a6ed9944 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -104,10 +104,10 @@ export class LinkManager {
         const batch = UndoManager.StartBatch("follow link click");
         // open up target if it's not already in view ...
         const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => {
-            const targetFocusAfterDocFocus = () => {
+            const targetFocusAfterDocFocus = (didFocus: boolean) => {
                 const where = StrCast(sourceDoc.followLinkLocation) || followLoc;
                 const hackToCallFinishAfterFocus = () => {
-                    finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout.
+                    finished && setTimeout(finished); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout.
                     return false; // we must return false here so that the zoom to the document is not reversed.  If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false
                 };
                 const addTab = docViewProps.addDocTab(doc, where);
@@ -118,7 +118,7 @@ export class LinkManager {
                 return where !== "inPlace" || addTab; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target)
             };
             if (!sourceDoc.followLinkZoom) {
-                targetFocusAfterDocFocus();
+                targetFocusAfterDocFocus(false);
             } else {
                 // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
                 docViewProps.focus(sourceDoc, BoolCast(sourceDoc.followLinkZoom, true), 1, targetFocusAfterDocFocus);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 6fdd569da..7ae4137d9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -913,27 +913,30 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 newState.initializers![this.Document[Id]] = { panX: px, panY: py };
                 HistoryUtil.pushState(newState);
             }
-
-            if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) {
-                // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
-                if (!doc.z) this.setPan(px, py, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", 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
-            }
-            Doc.BrushDoc(this.props.Document);
-
+            // focus on the document in the collection
+            const didMove = !doc.z && (px !== savedState.px || py !== savedState.py);
+            const focusSpeed = didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 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) this.setPan(px, py, `transform ${focusSpeed}ms`, 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
+            Doc.BrushDoc(this.rootDoc);
             !doc.hidden && Doc.linkFollowHighlight(doc);
-            this.props.focus(this.props.Document, undefined, undefined, (didFocus: boolean) => {
+
+            // focus the collection in its parent view.  the parent view after focusing determines whether to reset the view change within the collection
+            this.props.focus(this.rootDoc, willZoom, scale, (didFocus: boolean) => {
                 setTimeout(() => {
-                    if (afterFocus?.(didFocus || (px !== savedState.px || py !== savedState.py))) {
+                    if (afterFocus?.(didFocus || didMove)) {
+                        doc.hidden && Doc.UnHighlightDoc(doc);
                         this.Document._panX = savedState.px;
                         this.Document._panY = savedState.py;
                         this.Document[this.scaleFieldKey] = savedState.s;
                         this.Document._viewTransition = savedState.pt;
+                    } else {
+                        doc.hidden && Doc.UnHighlightDoc(doc);
                     }
-                    doc.hidden && Doc.UnHighlightDoc(doc);
-                }, px !== savedState.px || py !== savedState.py ? 500 : 0);
+                }, focusSpeed);
                 return false;
-            });
-        }
+            })
+        };
     }
 
     setPanIntoView = (doc: Doc) => {
-- 
cgit v1.2.3-70-g09d2


From 96d730a155cc9e49ee83c8e985fbc5de87e7959f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Feb 2021 11:23:55 -0500
Subject: fixes for focus'ing to enable resetting of all navigations if
 desired(eg. zooming into portal)

---
 src/Utils.ts                                       |  4 +--
 src/client/util/DocumentManager.ts                 | 23 ++++++++++----
 src/client/util/LinkManager.ts                     | 32 ++++++++++---------
 .../views/collections/CollectionStackingView.tsx   |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 36 +++++++++++-----------
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 7 files changed, 58 insertions(+), 43 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 770cef283..b6a59118a 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -193,9 +193,9 @@ export namespace Utils {
 
     export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number) {
         if (scrollTop + contextHgt < targetY + targetHgt * 1.1) {
-            return targetY + targetHgt * 1.1 - contextHgt;
+            return Math.ceil(targetY + targetHgt * 1.1 - contextHgt);
         } else if (scrollTop > targetY - targetHgt * .1) {
-            return Math.max(0, targetY - targetHgt * .1);
+            return Math.max(0, Math.floor(targetY - targetHgt * .1));
         }
     }
 
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5d3f8af57..816f7f6be 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -144,6 +144,10 @@ export class DocumentManager {
     ): Promise<void> => {
         const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
         const docView = getFirstDocView(targetDoc, originatingDoc);
+        const highlight = () => {
+            const finalDocView = getFirstDocView(targetDoc);
+            finalDocView && Doc.linkFollowHighlight(finalDocView.rootDoc);
+        };
         const focusAndFinish = (didFocus: boolean) => {
             if (originatingDoc?.isPushpin) {
                 if (!didFocus || targetDoc.hidden) {
@@ -153,13 +157,10 @@ export class DocumentManager {
                 targetDoc.hidden && (targetDoc.hidden = undefined);
                 docView?.select(false);
             }
+            highlight();
             finished?.();
             return false;
         };
-        const highlight = () => {
-            const finalDocView = getFirstDocView(targetDoc);
-            finalDocView && Doc.linkFollowHighlight(finalDocView.rootDoc);
-        };
         let annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
         const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
         const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined;
@@ -172,7 +173,12 @@ export class DocumentManager {
                 first.focus(targetDoc, false);
             }
         } else if (docView && (targetDocContextView || !targetDocContext)) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?      
-            docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) => focusAndFinish(didFocus));
+            docView.props.focus(docView.rootDoc, willZoom, undefined, (didFocus: boolean) =>
+                new Promise<boolean>(res => {
+                    focusAndFinish(didFocus);
+                    res();
+                })
+            );
         } else {
             if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
                 createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
@@ -190,7 +196,12 @@ export class DocumentManager {
                         const findView = (delay: number) => {
                             const retryDocView = getFirstDocView(targetDoc);  // 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
-                                retryDocView.props.focus(targetDoc, willZoom, undefined, focusAndFinish); // focus on the target in the context
+                                retryDocView.props.focus(targetDoc, willZoom, undefined, (didFocus: boolean) =>
+                                    new Promise<boolean>(res => {
+                                        focusAndFinish(didFocus);
+                                        res();
+                                    })
+                                ); // focus on the target in the context
                                 highlight();
                             } else if (delay > 1500) {
                                 // we didn't find the target, so it must have moved out of the context.  Go back to just creating it.
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8a6ed9944..58ccfe645 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -103,25 +103,29 @@ export class LinkManager {
     public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => {
         const batch = UndoManager.StartBatch("follow link click");
         // open up target if it's not already in view ...
-        const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => {
-            const targetFocusAfterDocFocus = (didFocus: boolean) => {
+        const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
+            const createTabForTarget = (didFocus: boolean) => new Promise<boolean>(res => {
                 const where = StrCast(sourceDoc.followLinkLocation) || followLoc;
-                const hackToCallFinishAfterFocus = () => {
-                    finished && setTimeout(finished); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout.
-                    return false; // we must return false here so that the zoom to the document is not reversed.  If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false
-                };
-                const addTab = docViewProps.addDocTab(doc, where);
-                addTab && setTimeout(() => {
+                docViewProps.addDocTab(doc, where);
+                setTimeout(() => {
                     const targDocView = DocumentManager.Instance.getFirstDocumentView(doc);
-                    targDocView?.props.focus(doc, BoolCast(sourceDoc.followLinkZoom, false), undefined, hackToCallFinishAfterFocus);
-                }); //  add the target and focus on it.
-                return where !== "inPlace" || addTab; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target)
-            };
+                    if (targDocView) {
+                        targDocView.props.focus(doc, BoolCast(sourceDoc.followLinkZoom, false), undefined, (didFocus: boolean) => {
+                            finished?.();
+                            res(true);
+                            return new Promise<boolean>(res2 => res2());
+                        });
+                    } else {
+                        res(where !== "inPlace"); // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target)
+                    }
+                });
+            });
+
             if (!sourceDoc.followLinkZoom) {
-                targetFocusAfterDocFocus(false);
+                createTabForTarget(false);
             } else {
                 // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
-                docViewProps.focus(sourceDoc, BoolCast(sourceDoc.followLinkZoom, true), 1, targetFocusAfterDocFocus);
+                docViewProps.focus(sourceDoc, BoolCast(sourceDoc.followLinkZoom, true), 1, createTabForTarget);
             }
         };
         LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 770aea362..9c12d5020 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -192,7 +192,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
             }
         }
         return this.props.styleProvider?.(doc, props, property);
-    };
+    }
     getDisplayDoc(doc: Doc, width: () => number) {
         const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc;
         const height = () => this.getDocHeight(doc);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7ae4137d9..eeb4e61aa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -49,6 +49,7 @@ import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
 import { StyleProp, StyleLayers } from "../../StyleProvider";
 import { DocumentDecorations } from "../../DocumentDecorations";
 import { FieldViewProps } from "../../nodes/FieldView";
+import { reset } from "colors";
 
 export const panZoomSchema = createSchema({
     _panX: "number",
@@ -908,8 +909,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const layoutdoc = Doc.Layout(doc);
             const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
             const newState = HistoryUtil.getState();
-            const { px, py } = layoutdoc.annotationOn ? savedState : willZoom ? this.setZoomToDoc(layoutdoc, scale) : this.setPanIntoView(doc);
-            if (!layoutdoc.annotationOn) {   // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
+            const { px, py } = layoutdoc.annotationOn || this.rootDoc._isGroup ? savedState : willZoom ? this.setZoomToDoc(layoutdoc, scale) : this.setPanIntoView(doc);
+            if (!layoutdoc.annotationOn && !this.rootDoc._isGroup) {   // 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: px, panY: py };
                 HistoryUtil.pushState(newState);
             }
@@ -921,22 +922,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             Doc.BrushDoc(this.rootDoc);
             !doc.hidden && Doc.linkFollowHighlight(doc);
 
-            // focus the collection in its parent view.  the parent view after focusing determines whether to reset the view change within the collection
-            this.props.focus(this.rootDoc, willZoom, scale, (didFocus: boolean) => {
-                setTimeout(() => {
-                    if (afterFocus?.(didFocus || didMove)) {
-                        doc.hidden && Doc.UnHighlightDoc(doc);
-                        this.Document._panX = savedState.px;
-                        this.Document._panY = savedState.py;
-                        this.Document[this.scaleFieldKey] = savedState.s;
-                        this.Document._viewTransition = savedState.pt;
-                    } else {
-                        doc.hidden && Doc.UnHighlightDoc(doc);
-                    }
-                }, focusSpeed);
-                return false;
-            })
-        };
+            // 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) => {
+                doc.hidden && Doc.UnHighlightDoc(doc);
+                const resetView = afterFocus ? await afterFocus(moved) : false;
+                if (resetView) {
+                    this.Document._panX = savedState.px;
+                    this.Document._panY = savedState.py;
+                    this.Document[this.scaleFieldKey] = savedState.s;
+                    this.Document._viewTransition = savedState.pt;
+                }
+                return resetView;
+            };
+            this.props.focus(this.rootDoc._isGroup ? doc : this.rootDoc, willZoom, scale, (didFocus: boolean) =>
+                new Promise<boolean>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), focusSpeed)));
+        }
     }
 
     setPanIntoView = (doc: Doc) => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d20866f94..3fc39d554 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -42,7 +42,7 @@ import { RadialMenu } from './RadialMenu';
 import React = require("react");
 import { LinkDocPreview } from "./LinkDocPreview";
 
-export type DocAfterFocusFunc = (notFocused: boolean) => boolean;
+export type DocAfterFocusFunc = (notFocused: boolean) => Promise<boolean>;
 export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => void;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any;
 export interface DocComponentView {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 4b926bb6f..989be1ab9 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -80,7 +80,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
         }
     }
 
-    scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => { this._pdfViewer?.scrollFocus(doc, smooth, afterFocus); }
+    scrollFocus = (doc: Doc, smooth: boolean, afterFocus?: DocAfterFocusFunc) => this._pdfViewer?.scrollFocus(doc, smooth, afterFocus);
     getAnchor = () => this.rootDoc;
     componentWillUnmount() { this._selectReactionDisposer?.(); }
     componentDidMount() {
-- 
cgit v1.2.3-70-g09d2


From 170b6bceb26e0837f7864cb4ff5266003d06f995 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Feb 2021 14:49:42 -0500
Subject: fixed pinning documents with a group to zoom to view properly.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 67 ++++++++++++----------
 src/client/views/nodes/DocumentView.tsx            |  6 +-
 2 files changed, 40 insertions(+), 33 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eeb4e61aa..9609cb826 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -789,6 +789,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
+        if (this.Document._isGroup) return;
         let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05;
         if (deltaScale < 0) deltaScale = -deltaScale;
         const [x, y] = this.getTransform().transformPoint(pointX, pointY);
@@ -815,10 +816,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             e.stopPropagation();
         }
         else if (this.props.active(true)) {
-            e.stopPropagation();
-            e.preventDefault();
-            if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true);
-            else this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+            if (!e.ctrlKey && MarqueeView.DragMarquee) {
+                this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true);
+                e.stopPropagation();
+                e.preventDefault();
+            }
+            else if (!this.Document._isGroup) {
+                e.stopPropagation();
+                e.preventDefault();
+                this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+            }
 
         }
         this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]);
@@ -876,6 +883,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     });
 
     scaleAtPt(docpt: number[], scale: number) {
+        if (this.Document._isGroup) return;
         const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         this.Document._viewTransition = "transform 500ms";
         this.layoutDoc[this.scaleFieldKey] = scale;
@@ -886,7 +894,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
     }
 
-    focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => {
+    focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => {
         const state = HistoryUtil.getState();
 
         // TODO This technically isn't correct if type !== "doc", as
@@ -906,10 +914,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         if (this.props.Document.scrollHeight) {
             this.props.focus(doc, undefined, undefined, afterFocus);
         } else {
+            const xfToCollection = docTransform ?? Transform.Identity();
             const layoutdoc = Doc.Layout(doc);
             const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
             const newState = HistoryUtil.getState();
-            const { px, py } = layoutdoc.annotationOn || this.rootDoc._isGroup ? savedState : willZoom ? this.setZoomToDoc(layoutdoc, scale) : this.setPanIntoView(doc);
+            const { px, py } = layoutdoc.annotationOn || this.rootDoc._isGroup ? savedState : this.setPanIntoView(layoutdoc, xfToCollection, willZoom ? scale || .75 : undefined);
             if (!layoutdoc.annotationOn && !this.rootDoc._isGroup) {   // 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: px, panY: py };
                 HistoryUtil.pushState(newState);
@@ -935,34 +944,32 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 return resetView;
             };
             this.props.focus(this.rootDoc._isGroup ? doc : this.rootDoc, willZoom, scale, (didFocus: boolean) =>
-                new Promise<boolean>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), focusSpeed)));
+                new Promise<boolean>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), focusSpeed)),
+                !this.rootDoc._isGroup ? Transform.Identity() :
+                    new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX), NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1)
+                        .transform(docTransform ?? Transform.Identity()));
         }
     }
 
-    setPanIntoView = (doc: Doc) => {
-        const cx = NumCast(this.props.Document._panX);
-        const cy = NumCast(this.props.Document._panY);
-        const pwid = this.props.PanelWidth() / NumCast(this.props.Document._viewScale, 1);
-        const phgt = this.props.PanelHeight() / NumCast(this.props.Document._viewScale, 1);
-        const screen = { left: cx - pwid / 2, right: cx + pwid / 2, top: cy - phgt / 2, bot: cy + phgt / 2 };
-        const bounds = { left: NumCast(doc.x) - pwid / 10, right: NumCast(doc.x) + doc[WidthSym]() + pwid / 10, top: NumCast(doc.y) - phgt / 10, bot: NumCast(doc.y) + doc[HeightSym]() + phgt / 10 };
-        return {
-            px: cx + Math.min(0, bounds.left - screen.left) + Math.max(0, bounds.right - screen.right),
-            py: cy + Math.min(0, bounds.top - screen.top) + Math.max(0, bounds.bot - screen.bot)
-        };
-    }
+    setPanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
+        const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1);
+        const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1);
+        const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y));
+        const pt2 = xf.transformPoint(NumCast(doc.x) + doc[WidthSym](), NumCast(doc.y) + doc[HeightSym]());
+        const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1] };
 
-    setZoomToDoc = (doc: Doc, scale: number = 0.75) => {
-        const pw = this.props.PanelWidth();
-        const ph = this.props.PanelHeight();
-        pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height)));
-        const pwid = pw / NumCast(this.props.Document._viewScale, 1);
-        const phgt = ph / NumCast(this.props.Document._viewScale, 1);
-        const bounds = { left: NumCast(doc.x) - pwid / 10, right: NumCast(doc.x) + doc[WidthSym]() + pwid / 10, top: NumCast(doc.y) - phgt / 10, bot: NumCast(doc.y) + doc[HeightSym]() + phgt / 10 };
-        return {
-            px: (bounds.left + bounds.right) / 2,
-            py: (bounds.top + bounds.bot) / 2
-        };
+        if (scale) {
+            this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / doc[WidthSym](), this.props.PanelHeight() / doc[HeightSym]());
+            return { px: (bounds.left + bounds.right) / 2, py: (bounds.top + bounds.bot) / 2 };
+        } else {
+            const cx = NumCast(this.layoutDoc._panX);
+            const cy = NumCast(this.layoutDoc._panY);
+            const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
+            return {
+                px: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
+                py: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot)
+            };
+        }
     }
 
     onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3fc39d554..34e0a2bc8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -43,7 +43,7 @@ import React = require("react");
 import { LinkDocPreview } from "./LinkDocPreview";
 
 export type DocAfterFocusFunc = (notFocused: boolean) => Promise<boolean>;
-export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => void;
+export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => void;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any;
 export interface DocComponentView {
     getAnchor: () => Doc;
@@ -375,11 +375,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     }
 
-    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc) => {
+    focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => {
         if (this._componentView?.scrollFocus) {
             return this._componentView?.scrollFocus?.(doc, !LinkDocPreview.LinkInfo, afterFocus); // bcz: smooth parameter should really be passed into focus() instead of inferred here
         }
-        return this.props.focus(doc, willZoom, scale, afterFocus);
+        return this.props.focus(doc, willZoom, scale, afterFocus, docTransform);
     }
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 &&
-- 
cgit v1.2.3-70-g09d2


From 890337b525ea460f9986562c047135bc5ca203a6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 11 Feb 2021 16:15:31 -0500
Subject: made zooming on annotation in an image  in a freeform view act like
 zooming on a group

---
 .../collectionFreeForm/CollectionFreeFormView.tsx    | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9609cb826..bba0807a4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -918,8 +918,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             const layoutdoc = Doc.Layout(doc);
             const savedState = { px: NumCast(this.Document._panX), py: NumCast(this.Document._panY), s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
             const newState = HistoryUtil.getState();
-            const { px, py } = layoutdoc.annotationOn || this.rootDoc._isGroup ? savedState : this.setPanIntoView(layoutdoc, xfToCollection, willZoom ? scale || .75 : undefined);
-            if (!layoutdoc.annotationOn && !this.rootDoc._isGroup) {   // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
+            const cantTransform = this.props.isAnnotationOverlay || this.rootDoc._isGroup;
+            const { px, py } = cantTransform ? savedState : this.setPanIntoView(layoutdoc, xfToCollection, willZoom ? scale || .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: px, panY: py };
                 HistoryUtil.pushState(newState);
             }
@@ -943,11 +944,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 }
                 return resetView;
             };
-            this.props.focus(this.rootDoc._isGroup ? doc : this.rootDoc, willZoom, scale, (didFocus: boolean) =>
-                new Promise<boolean>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), focusSpeed)),
-                !this.rootDoc._isGroup ? Transform.Identity() :
-                    new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX), NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1)
-                        .transform(docTransform ?? Transform.Identity()));
+            const xf = !cantTransform ? Transform.Identity() :
+                this.props.isAnnotationOverlay ?
+                    new Transform(NumCast(this.rootDoc.x), NumCast(this.rootDoc.y), this.rootDoc[WidthSym]() / Doc.NativeWidth(this.rootDoc))
+                    :
+                    new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX),
+                        NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1);
+            this.props.focus(cantTransform ? doc : this.rootDoc, willZoom, scale, (didFocus: boolean) =>
+                new Promise<boolean>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), focusSpeed)), xf.transform(docTransform ?? Transform.Identity()));
         }
     }
 
@@ -959,7 +963,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1] };
 
         if (scale) {
-            this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / doc[WidthSym](), this.props.PanelHeight() / doc[HeightSym]());
+            this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / Math.abs(pt2[0] - pt[0])), this.props.PanelHeight() / Math.abs(pt2[1] - pt[1]);
             return { px: (bounds.left + bounds.right) / 2, py: (bounds.top + bounds.bot) / 2 };
         } else {
             const cx = NumCast(this.layoutDoc._panX);
-- 
cgit v1.2.3-70-g09d2

