From a5bf418707321711559782f0348063681c553cbb Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Wed, 10 Aug 2022 16:11:23 -0400
Subject: added placeholder text box for files being uploaded

---
 src/client/views/collections/CollectionSubView.tsx | 43 +++++++++++++++++++++-
 1 file changed, 42 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5479929bd..35a14704d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -448,6 +448,46 @@ export function CollectionSubView<X>(moreProps?: X) {
             }
             this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
         }
+
+        /**
+         * Creates a placeholder doc view for files being uploaded and removes placeholder docs once files are uplodaded.
+         *
+         * @param files the files to upload that we want to create placeholders for
+         * @param options the document options (primarily the x and y coordinates to put doc)
+         * @param text in the case of youtube the text is the url to the video
+         * @returns a disposer action that removes the placeholders created after files get uploaded
+         */
+        placeHolderDisposer = (files: File[] | string, options: DocumentOptions, text: string) => {
+            // TODO: nda - create a specialized view for placeholder upload with a spinner and ability to retry upload
+            let placeholders: Doc[] = [];
+            // handle yt case
+            if (typeof files === 'string') {
+                placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 500, _height: 200 }));
+            } else {
+                // every other doc type is an array of File
+
+                // Get the file names as a text
+                let textStr = '';
+                files.forEach(file => {
+                    textStr += file.name + '\n';
+                });
+                placeholders.push(Docs.Create.TextDocument('Loading: \n' + textStr, { ...options, title: files.length + ' files', _width: 500, _height: files.length * 40 }));
+            }
+            // disposer action to remove placeholders once files are uploaded
+            const remove = action(() => {
+                if (!this.props.DataDoc) {
+                    return;
+                }
+                for (let i = 0; i < placeholders.length; i++) {
+                    Doc.RemoveDocFromList(this.props.DataDoc, 'data', placeholders[i]);
+                }
+            });
+            placeholders.forEach(pl => {
+                this.addDocument(pl);
+            });
+            return remove;
+        };
+
         slowLoadDocuments = async (
             files: File[] | string,
             options: DocumentOptions,
@@ -458,7 +498,8 @@ export function CollectionSubView<X>(moreProps?: X) {
             clientY: number,
             addDocument: (doc: Doc | Doc[]) => boolean
         ) => {
-            const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
+            const disposer = this.placeHolderDisposer(files, options, text);
+            // const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
             if (typeof files === 'string') {
                 generatedDocuments.push(...(await DocUtils.uploadYoutubeVideo(files, options)));
             } else {
-- 
cgit v1.2.3-70-g09d2


From 6c349731fb84f1c44993d2bc55410da19db29a07 Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Wed, 10 Aug 2022 16:16:00 -0400
Subject: added placeholder text box for files being uploaded

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

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 35a14704d..2f3f57bac 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -462,7 +462,7 @@ export function CollectionSubView<X>(moreProps?: X) {
             let placeholders: Doc[] = [];
             // handle yt case
             if (typeof files === 'string') {
-                placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 500, _height: 200 }));
+                placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 400, _height: 30 }));
             } else {
                 // every other doc type is an array of File
 
-- 
cgit v1.2.3-70-g09d2


From 820d40eea4eeb5977889e0ef6c35f9092df44b4b Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Wed, 10 Aug 2022 17:08:47 -0400
Subject: created placeholder loading box but have to get location of final doc
 to update with loading box

---
 src/client/documents/DocumentTypes.ts              |  3 ++-
 src/client/documents/Documents.ts                  | 18 +++++++++++++
 src/client/views/collections/CollectionSubView.tsx |  8 ++++--
 src/client/views/nodes/DocumentContentsView.tsx    |  2 ++
 src/client/views/nodes/LoadingBox.scss             |  7 +++++
 src/client/views/nodes/LoadingBox.tsx              | 31 ++++++++++++++++++++++
 6 files changed, 66 insertions(+), 3 deletions(-)
 create mode 100644 src/client/views/nodes/LoadingBox.scss
 create mode 100644 src/client/views/nodes/LoadingBox.tsx

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 9dfadf778..d99cd2dac 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -26,6 +26,7 @@ export enum DocumentType {
     FUNCPLOT = 'funcplot', // function plotter
     MAP = 'map',
     DATAVIZ = 'dataviz',
+    LOADING = 'loading',
 
     // special purpose wrappers that either take no data or are compositions of lower level types
     LINK = 'link',
@@ -63,5 +64,5 @@ export enum CollectionViewType {
     Grid = 'grid',
     Pile = 'pileup',
     StackedTimeline = 'stacked timeline',
-    NoteTaking = "notetaking"
+    NoteTaking = 'notetaking',
 }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c7ea04839..6ccb4358a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -48,6 +48,7 @@ import { KeyValueBox } from '../views/nodes/KeyValueBox';
 import { LabelBox } from '../views/nodes/LabelBox';
 import { LinkBox } from '../views/nodes/LinkBox';
 import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
+import { LoadingBox } from '../views/nodes/LoadingBox';
 import { MapBox } from '../views/nodes/MapBox/MapBox';
 import { PDFBox } from '../views/nodes/PDFBox';
 import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox';
@@ -648,6 +649,13 @@ export namespace Docs {
                     options: { _fitWidth: true, nativeDimModifiable: true, links: '@links(self)' },
                 },
             ],
+            [
+                DocumentType.LOADING,
+                {
+                    layout: { view: LoadingBox, dataField: defaultDataKey },
+                    options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
+                },
+            ],
         ]);
 
         const suffix = 'Proto';
@@ -875,6 +883,16 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
         }
 
+        export function LoadingDocument(title: string, text: string, width?: number, height?: number, options: DocumentOptions = {}) {
+            let myWidth = 300;
+            let myHeight = 300;
+            if (height && width) {
+                myWidth = width;
+                myHeight = height;
+            }
+            return InstanceFromProto(Prototypes.get(DocumentType.LOADING), '', { ...options, title, text, _width: myWidth, _height: myHeight });
+        }
+
         export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
             return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
         }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 2f3f57bac..79f629072 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -462,7 +462,8 @@ export function CollectionSubView<X>(moreProps?: X) {
             let placeholders: Doc[] = [];
             // handle yt case
             if (typeof files === 'string') {
-                placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 400, _height: 30 }));
+                placeholders.push(Docs.Create.LoadingDocument('Loading...', text, 500, 500, { ...options }));
+                // placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 400, _height: 30 }));
             } else {
                 // every other doc type is an array of File
 
@@ -471,7 +472,9 @@ export function CollectionSubView<X>(moreProps?: X) {
                 files.forEach(file => {
                     textStr += file.name + '\n';
                 });
-                placeholders.push(Docs.Create.TextDocument('Loading: \n' + textStr, { ...options, title: files.length + ' files', _width: 500, _height: files.length * 40 }));
+                placeholders.push(Docs.Create.LoadingDocument('Loading...', textStr, 500, 500, { ...options }));
+
+                // placeholders.push(Docs.Create.TextDocument('Loading: \n' + textStr, { ...options, title: files.length + ' files', _width: 500, _height: files.length * 40 }));
             }
             // disposer action to remove placeholders once files are uploaded
             const remove = action(() => {
@@ -498,6 +501,7 @@ export function CollectionSubView<X>(moreProps?: X) {
             clientY: number,
             addDocument: (doc: Doc | Doc[]) => boolean
         ) => {
+            // TODO: once loading thing is moved it should update the x and y of the file it is placeholder for
             const disposer = this.placeHolderDisposer(files, options, text);
             // const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
             if (typeof files === 'string') {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 381436a56..4d4985f2a 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -43,6 +43,7 @@ import { VideoBox } from './VideoBox';
 import { WebBox } from './WebBox';
 import React = require('react');
 import XRegExp = require('xregexp');
+import { LoadingBox } from './LoadingBox';
 
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
@@ -267,6 +268,7 @@ export class DocumentContentsView extends React.Component<
                     DataVizBox,
                     HTMLtag,
                     ComparisonBox,
+                    LoadingBox,
                 }}
                 bindings={bindings}
                 jsx={layoutFrame}
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
new file mode 100644
index 000000000..239faa78e
--- /dev/null
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -0,0 +1,7 @@
+.loadingBoxContainer {
+    display: flex;
+    flex-direction: column;
+    align-content: center;
+    justify-content: center;
+    background-color: #fdfdfd;
+}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
new file mode 100644
index 000000000..0e0619241
--- /dev/null
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -0,0 +1,31 @@
+import { observer } from 'mobx-react';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from './FieldView';
+import * as React from 'react';
+import './LoadingBox.scss';
+import ReactLoading from 'react-loading';
+
+export interface LoadingBoxProps {
+    title: string;
+    text: string;
+}
+
+@observer
+export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(LoadingBox, fieldKey);
+    }
+
+    constructor(props: any) {
+        super(props);
+    }
+
+    render() {
+        return (
+            <div className="loadingBoxContainer">
+                <span>Loading: {this.dataDoc.text}</span>
+                <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />
+            </div>
+        );
+    }
+}
-- 
cgit v1.2.3-70-g09d2


From bb2b38b0e47eaf8e64554d101b605bf35a178239 Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Tue, 16 Aug 2022 16:43:01 -0400
Subject: updated placeholder

---
 src/.DS_Store                                      | Bin 10244 -> 10244 bytes
 src/client/documents/Documents.ts                  |  63 +++++++++++++------
 src/client/util/CurrentUserUtils.ts                |   1 +
 src/client/views/collections/CollectionSubView.tsx |  70 +++++++--------------
 src/client/views/nodes/LoadingBox.tsx              |  25 ++++++++
 5 files changed, 94 insertions(+), 65 deletions(-)

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

diff --git a/src/.DS_Store b/src/.DS_Store
index 4751acf44..4ed785983 100644
Binary files a/src/.DS_Store and b/src/.DS_Store differ
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6ccb4358a..d8497e3af 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,4 +1,5 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { files } from 'jszip';
 import { action, runInAction } from 'mobx';
 import { basename } from 'path';
 import { DateField } from '../../fields/DateField';
@@ -794,7 +795,7 @@ export namespace Docs {
          * only when creating a DockDocument from the current user's already existing
          * main document.
          */
-        function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string) {
+        function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) {
             const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_"
             const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
 
@@ -813,13 +814,22 @@ export namespace Docs {
             // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
             dataProps[fieldKey + '-annotations'] = new List<Doc>();
             dataProps[fieldKey + '-sidebar'] = new List<Doc>();
-            const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
+
+            const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
+
+            if (placeholderDoc) {
+                dataDoc.proto = proto;
+            }
 
             const viewFirstProps: { [id: string]: any } = {};
             viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             viewFirstProps['acl-Override'] = 'None';
             viewFirstProps.author = Doc.CurrentUserEmail;
-            const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
+            let viewDoc: Doc;
+            if (placeholderDoc) {
+                viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
+            }
+            viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
             Doc.assign(viewDoc, viewProps, true, true);
             ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
 
@@ -883,14 +893,12 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
         }
 
-        export function LoadingDocument(title: string, text: string, width?: number, height?: number, options: DocumentOptions = {}) {
-            let myWidth = 300;
-            let myHeight = 300;
-            if (height && width) {
-                myWidth = width;
-                myHeight = height;
-            }
-            return InstanceFromProto(Prototypes.get(DocumentType.LOADING), '', { ...options, title, text, _width: myWidth, _height: myHeight });
+        export const filesToDocs = new Map<Doc, File>();
+
+        export function LoadingDocument(file: File, options: DocumentOptions, ytString?: string) {
+            const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), file.name, { _height: 300, _width: 300, ...options });
+            // filesToDocs.set(loading, file);
+            return loading;
         }
 
         export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
@@ -969,13 +977,13 @@ export namespace Docs {
             return I;
         }
 
-        export function PdfDocument(url: string, options: DocumentOptions = {}) {
+        export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             const width = options._width || undefined;
             const height = options._height || undefined;
             const nwid = options._nativeWidth || undefined;
             const nhght = options._nativeHeight || undefined;
             if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width);
-            return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options);
+            return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function WebDocument(url: string, options: DocumentOptions = {}) {
@@ -1462,8 +1470,8 @@ export namespace DocUtils {
         return created;
     }
 
-    export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
-        let ctor: ((path: string, options: DocumentOptions) => Doc | Promise<Doc | undefined>) | undefined = undefined;
+    export async function DocumentFromType(type: string, path: string, options: DocumentOptions, rootDoc?: Doc): Promise<Opt<Doc>> {
+        let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined = undefined;
         if (type.indexOf('image') !== -1) {
             ctor = Docs.Create.ImageDocument;
             if (!options._width) options._width = 300;
@@ -1512,7 +1520,7 @@ export namespace DocUtils {
             options = { ...options, _width: 400, _height: 512, title: path };
         }
 
-        return ctor ? ctor(path, options) : undefined;
+        return ctor ? ctor(path, options, rootDoc) : undefined;
     }
 
     export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
@@ -1728,14 +1736,14 @@ export namespace DocUtils {
         return dd;
     }
 
-    async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) {
+    async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, rootDoc?: Doc) {
         if (result instanceof Error) {
             alert(`Upload failed: ${result.message}`);
             return;
         }
         const full = { ...options, _width: 400, title: name };
         const pathname = Utils.prepend(result.accessPaths.agnostic.client);
-        const doc = await DocUtils.DocumentFromType(type, pathname, full);
+        const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc);
         if (doc) {
             const proto = Doc.GetProto(doc);
             proto.text = result.rawText;
@@ -1813,6 +1821,25 @@ export namespace DocUtils {
         return generatedDocuments;
     }
 
+    export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
+        const generatedDocuments: Doc[] = [];
+        Networking.UploadFilesToServer([file]).then(upfiles => {
+            const {
+                source: { name, type },
+                result,
+            } = upfiles.lastElement();
+            console.log(name, type);
+            name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
+        });
+    }
+
+    export function generatePlaceHolder(file: File, options: DocumentOptions) {
+        return Docs.Create.LoadingDocument(file, options);
+        // placeholder.file = file
+        // TODO: nda - modify loading doc so it only takes in options
+        // Docs.Create.LoadingDocument(options, )
+    }
+
     // copies the specified drag factory document
     export function copyDragFactory(dragFactory: Doc) {
         if (!dragFactory) return undefined;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d19874720..492513a61 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -196,6 +196,7 @@ export class CurrentUserUtils {
             makeIconTemplate(DocumentType.RTF,   "text",  { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}),
             makeIconTemplate(DocumentType.IMG,   "data",  { iconTemplate:DocumentType.IMG,   _height: undefined}),
             makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
+            makeIconTemplate(DocumentType.COL,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.VID,   "icon",  { iconTemplate:DocumentType.IMG}),
             makeIconTemplate(DocumentType.BUTTON,"data",  { iconTemplate:DocumentType.FONTICON}),
             //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 79f629072..1ff4f5ab8 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -449,48 +449,6 @@ export function CollectionSubView<X>(moreProps?: X) {
             this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
         }
 
-        /**
-         * Creates a placeholder doc view for files being uploaded and removes placeholder docs once files are uplodaded.
-         *
-         * @param files the files to upload that we want to create placeholders for
-         * @param options the document options (primarily the x and y coordinates to put doc)
-         * @param text in the case of youtube the text is the url to the video
-         * @returns a disposer action that removes the placeholders created after files get uploaded
-         */
-        placeHolderDisposer = (files: File[] | string, options: DocumentOptions, text: string) => {
-            // TODO: nda - create a specialized view for placeholder upload with a spinner and ability to retry upload
-            let placeholders: Doc[] = [];
-            // handle yt case
-            if (typeof files === 'string') {
-                placeholders.push(Docs.Create.LoadingDocument('Loading...', text, 500, 500, { ...options }));
-                // placeholders.push(Docs.Create.TextDocument('Loading: ' + text, { ...options, title: text, _width: 400, _height: 30 }));
-            } else {
-                // every other doc type is an array of File
-
-                // Get the file names as a text
-                let textStr = '';
-                files.forEach(file => {
-                    textStr += file.name + '\n';
-                });
-                placeholders.push(Docs.Create.LoadingDocument('Loading...', textStr, 500, 500, { ...options }));
-
-                // placeholders.push(Docs.Create.TextDocument('Loading: \n' + textStr, { ...options, title: files.length + ' files', _width: 500, _height: files.length * 40 }));
-            }
-            // disposer action to remove placeholders once files are uploaded
-            const remove = action(() => {
-                if (!this.props.DataDoc) {
-                    return;
-                }
-                for (let i = 0; i < placeholders.length; i++) {
-                    Doc.RemoveDocFromList(this.props.DataDoc, 'data', placeholders[i]);
-                }
-            });
-            placeholders.forEach(pl => {
-                this.addDocument(pl);
-            });
-            return remove;
-        };
-
         slowLoadDocuments = async (
             files: File[] | string,
             options: DocumentOptions,
@@ -501,13 +459,31 @@ export function CollectionSubView<X>(moreProps?: X) {
             clientY: number,
             addDocument: (doc: Doc | Doc[]) => boolean
         ) => {
+            // create placeholder docs
+            // inside placeholder docs have some func that
+
             // TODO: once loading thing is moved it should update the x and y of the file it is placeholder for
-            const disposer = this.placeHolderDisposer(files, options, text);
+            let pileUpDoc = undefined;
             // const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
             if (typeof files === 'string') {
-                generatedDocuments.push(...(await DocUtils.uploadYoutubeVideo(files, options)));
+                // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
+                // generatedDocuments.push(Docs.Create.LoadingDocument(files, options));
             } else {
-                generatedDocuments.push(...(await DocUtils.uploadFilesToDocs(files, options)));
+                // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
+                generatedDocuments.push(
+                    ...files.map(file => {
+                        const loading = Docs.Create.LoadingDocument(file, options);
+                        // now that there is a doc do whatever slowload was going to do with that file
+                        if (typeof file === 'string') {
+                            // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
+                            // (await DocUtils.uploadYoutubeVideo(files, options)));
+                        } else {
+                            // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
+                            DocUtils.uploadFileToDoc(file, {}, loading);
+                        }
+                        return loading;
+                    })
+                );
             }
             if (generatedDocuments.length) {
                 // Creating a dash document
@@ -523,7 +499,8 @@ export function CollectionSubView<X>(moreProps?: X) {
                 if (completed) completed(set);
                 else {
                     if (isFreeformView && generatedDocuments.length > 1) {
-                        addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!);
+                        pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!;
+                        addDocument(pileUpDoc);
                     } else {
                         generatedDocuments.forEach(addDocument);
                     }
@@ -535,7 +512,6 @@ export function CollectionSubView<X>(moreProps?: X) {
                     alert('Document upload failed - possibly an unsupported file type.');
                 }
             }
-            disposer();
         };
     }
 
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 0e0619241..9d4668dde 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -4,6 +4,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import * as React from 'react';
 import './LoadingBox.scss';
 import ReactLoading from 'react-loading';
+import { Docs, DocUtils } from '../../documents/Documents';
 
 export interface LoadingBoxProps {
     title: string;
@@ -16,6 +17,30 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(LoadingBox, fieldKey);
     }
 
+    componentDidMount() {
+        // const file = Docs.Create.filesToDocs.get(this.rootDoc);
+        // if (file) {
+        //     console.log('Got to file');
+        //     Docs.Create.filesToDocs.delete(this.rootDoc);
+        //     // now that there is a doc do whatever slowload was going to do with that file
+        //     if (typeof file === 'string') {
+        //         // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
+        //         // (await DocUtils.uploadYoutubeVideo(files, options)));
+        //     } else {
+        //         // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
+        //         DocUtils.uploadFileToDoc(file, {}, this.rootDoc);
+        //     }
+        // } else {
+        //     // check if file now exists on server or not
+        //     // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading)
+        //     // if it doesn't display an error message "upload failed"
+        // }
+        // query endpoints to:
+        // check if file now exists on server or not
+        // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading)
+        // if it doesn't display an error message "upload failed"
+    }
+
     constructor(props: any) {
         super(props);
     }
-- 
cgit v1.2.3-70-g09d2


From 27945b9a931fa9504404174dd08964556dc3aea2 Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Tue, 16 Aug 2022 21:16:03 -0400
Subject: added loading for diff doc types

---
 src/client/documents/Documents.ts                  | 47 ++++++++++++++++------
 src/client/views/collections/CollectionSubView.tsx | 11 ++---
 src/client/views/nodes/LoadingBox.scss             |  5 +++
 src/client/views/nodes/LoadingBox.tsx              | 12 +++---
 4 files changed, 48 insertions(+), 27 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d8497e3af..e54fe16de 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -844,9 +844,9 @@ export namespace Docs {
             return viewDoc;
         }
 
-        export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}) {
+        export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             const imgField = url instanceof ImageField ? url : new ImageField(url);
-            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options });
+            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function PresDocument(options: DocumentOptions = {}) {
@@ -857,12 +857,12 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined });
         }
 
-        export function VideoDocument(url: string, options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options);
+        export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
+            return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
 
-        export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options);
+        export function YoutubeDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
+            return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function WebCamDocument(url: string, options: DocumentOptions = {}) {
@@ -877,8 +877,16 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options);
         }
 
-        export function AudioDocument(url: string, options: DocumentOptions = {}) {
-            return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any });
+        export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
+            return InstanceFromProto(
+                Prototypes.get(DocumentType.AUDIO),
+                new AudioField(url),
+                { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any },
+                undefined,
+                undefined,
+                undefined,
+                overwriteDoc
+            );
         }
 
         export function RecordingDocument(url: string, options: DocumentOptions = {}) {
@@ -895,8 +903,10 @@ export namespace Docs {
 
         export const filesToDocs = new Map<Doc, File>();
 
-        export function LoadingDocument(file: File, options: DocumentOptions, ytString?: string) {
-            const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), file.name, { _height: 300, _width: 300, ...options });
+        export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) {
+            const fileName = typeof file == 'string' ? file : file.name;
+            options.title = fileName;
+            const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 150, ...options });
             // filesToDocs.set(loading, file);
             return loading;
         }
@@ -1114,8 +1124,8 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) });
         }
 
-        export function DataVizDocument(url: string, options?: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options });
+        export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) {
+            return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
@@ -1809,6 +1819,18 @@ export namespace DocUtils {
         }
         return generatedDocuments;
     }
+
+    export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) {
+        const generatedDocuments: Doc[] = [];
+        Networking.UploadYoutubeToServer(videoId).then(upfiles => {
+            const {
+                source: { name, type },
+                result,
+            } = upfiles.lastElement();
+            name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
+        });
+    }
+
     export async function uploadFilesToDocs(files: File[], options: DocumentOptions) {
         const generatedDocuments: Doc[] = [];
         const upfiles = await Networking.UploadFilesToServer(files);
@@ -1828,7 +1850,6 @@ export namespace DocUtils {
                 source: { name, type },
                 result,
             } = upfiles.lastElement();
-            console.log(name, type);
             name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
         });
     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 1ff4f5ab8..30467efa0 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -468,19 +468,16 @@ export function CollectionSubView<X>(moreProps?: X) {
             if (typeof files === 'string') {
                 // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
                 // generatedDocuments.push(Docs.Create.LoadingDocument(files, options));
+                const loading = Docs.Create.LoadingDocument(files, options);
+                generatedDocuments.push(loading);
+                DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
                 generatedDocuments.push(
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
                         // now that there is a doc do whatever slowload was going to do with that file
-                        if (typeof file === 'string') {
-                            // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
-                            // (await DocUtils.uploadYoutubeVideo(files, options)));
-                        } else {
-                            // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
-                            DocUtils.uploadFileToDoc(file, {}, loading);
-                        }
+                        DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
                     })
                 );
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index 239faa78e..e8890cd82 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -5,3 +5,8 @@
     justify-content: center;
     background-color: #fdfdfd;
 }
+
+.text {
+    text-overflow: ellipsis;
+    text-align: center;
+}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 9d4668dde..96620aff9 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -4,12 +4,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import * as React from 'react';
 import './LoadingBox.scss';
 import ReactLoading from 'react-loading';
-import { Docs, DocUtils } from '../../documents/Documents';
-
-export interface LoadingBoxProps {
-    title: string;
-    text: string;
-}
+import { StrCast } from '../../../fields/Types';
 
 @observer
 export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -18,6 +13,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
 
     componentDidMount() {
+        console.log(this.rootDoc);
         // const file = Docs.Create.filesToDocs.get(this.rootDoc);
         // if (file) {
         //     console.log('Got to file');
@@ -48,7 +44,9 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     render() {
         return (
             <div className="loadingBoxContainer">
-                <span>Loading: {this.dataDoc.text}</span>
+                <p className="text">Loading:</p>
+                <br></br>
+                <p className="text">{StrCast(this.rootDoc.title)}</p>
                 <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From 0178de4ab9ffd11630b700f9c02468b74beabd14 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 17 Aug 2022 12:31:26 -0400
Subject: fixed dragging docs on web and pdf to work better by temporarily
 adding transparent docs to the opaque layer so they can get drop events.  
 cleaned eraser and pen interaction code and made erasing strokes work faster
 and avoid hanging by not intersecting strokes that are already partially
 deleted.

---
 src/Utils.ts                                       |   3 +-
 src/client/documents/Documents.ts                  |  26 +---
 src/client/views/collections/CollectionSubView.tsx |  12 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 140 ++++++---------------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   5 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/VideoBox.tsx                |  11 +-
 src/client/views/nodes/WebBox.scss                 |  17 +--
 src/client/views/nodes/WebBox.tsx                  | 110 ++++++++--------
 src/client/views/pdf/PDFViewer.tsx                 |  75 +++++------
 10 files changed, 153 insertions(+), 252 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 528a429d0..9e002ebd4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -111,6 +111,7 @@ export namespace Utils {
 
     const isTransparentFunctionHack = 'isTransparent(__value__)';
     export const noRecursionHack = '__noRecursion';
+    export const noDragsDocFilter = 'noDragDocs:any:check';
     export function IsRecursiveFilter(val: string) {
         return !val.includes(noRecursionHack);
     }
@@ -125,7 +126,7 @@ export namespace Utils {
         // bcz: isTransparent(__value__) is a hack.  it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
         return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`; // bcz: hack.  noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
     }
-    export function PropUnsetFilter(prop: string) {
+    export function IsPropUnsetFilter(prop: string) {
         return `${prop}:any,${noRecursionHack}:unset`;
     }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cf9ed43e1..e579bfd8a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1143,30 +1143,6 @@ export namespace Docs {
 }
 
 export namespace DocUtils {
-    export function Excluded(d: Doc, docFilters: string[]) {
-        const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
-        docFilters.forEach(filter => {
-            const fields = filter.split(':');
-            const key = fields[0];
-            const value = fields[1];
-            const modifiers = fields[2];
-            if (!filterFacets[key]) {
-                filterFacets[key] = {};
-            }
-            filterFacets[key][value] = modifiers;
-        });
-
-        if (d.z) return false;
-        for (const facetKey of Object.keys(filterFacets)) {
-            const facet = filterFacets[facetKey];
-            const xs = Object.keys(facet).filter(value => facet[value] === 'x');
-            const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value));
-            if (failsNotEqualFacets) {
-                return true;
-            }
-        }
-        return false;
-    }
     /**
      * @param docs
      * @param docFilters
@@ -1200,7 +1176,7 @@ export namespace DocUtils {
                       return false;
                   }
 
-                  for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies')) {
+                  for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(':')[0])) {
                       const facet = filterFacets[facetKey];
 
                       // facets that match some value in the field of the document (e.g. some text field)
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5479929bd..e33bb77de 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -88,11 +88,11 @@ export function CollectionSubView<X>(moreProps?: X) {
         }
         collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters);
         collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []);
+        // child filters apply to the descendants of the documents in this collection
         childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
+        // unrecursive filters apply to the documents in the collection, but no their children.  See Utils.noRecursionHack
         unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
         childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()];
-        IsFiltered = () =>
-            this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined;
         searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
         @computed.struct get childDocs() {
             TraceMobx();
@@ -122,13 +122,11 @@ export function CollectionSubView<X>(moreProps?: X) {
                 return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one
             }
 
-            // console.log(Doc.ActiveDashboard._docFilters);
-            // if (!this.props.Document._docFilters && this.props.Document.currentFilter) {
-            //     (this.props.Document.currentFilter as Doc).filterBoolean = (this.props.ContainingCollectionDoc?.currentFilter as Doc)?.filterBoolean;
-            // }
             const docsforFilter: Doc[] = [];
             childDocs.forEach(d => {
-                // if (DocUtils.Excluded(d, docFilters)) return;
+                // dragging facets
+                const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter));
+                if (dragged && DragManager.docsBeingDragged.includes(d)) return false;
                 let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0;
                 if (notFiltered) {
                     notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f3074543b..45a5e30ff 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -109,8 +109,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable _hLines: number[] | undefined;
     @observable _vLines: number[] | undefined;
     @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
-    @observable _pullCoords: number[] = [0, 0];
-    @observable _pullDirection: string = '';
     @observable _showAnimTimeline = false;
     @observable _clusterSets: Doc[][] = [];
     @observable _deleteList: DocumentView[] = [];
@@ -465,20 +463,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     getClusterColor = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => {
         let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props'  used to be renderDepth + 1
-        if (property !== StyleProp.BackgroundColor) return styleProp;
-        const cluster = NumCast(doc?.cluster);
-        if (this.Document._useClusters) {
-            if (this._clusterSets.length <= cluster) {
-                setTimeout(() => doc && this.updateCluster(doc));
-            } else {
-                // choose a cluster color from a palette
-                const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
-                styleProp = colors[cluster % colors.length];
-                const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
-                // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
-                set?.map(s => (styleProp = StrCast(s.backgroundColor)));
-            }
-        } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray";
+        switch (property) {
+            case StyleProp.BackgroundColor:
+                const cluster = NumCast(doc?.cluster);
+                if (this.Document._useClusters) {
+                    if (this._clusterSets.length <= cluster) {
+                        setTimeout(() => doc && this.updateCluster(doc));
+                    } else {
+                        // choose a cluster color from a palette
+                        const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
+                        styleProp = colors[cluster % colors.length];
+                        const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
+                        // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
+                        set?.map(s => (styleProp = StrCast(s.backgroundColor)));
+                    }
+                }
+                break;
+        }
         return styleProp;
     };
 
@@ -523,17 +524,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.Pen:
                         break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
                     case InkTool.Eraser:
-                        document.addEventListener('pointermove', this.onEraserMove);
-                        document.addEventListener('pointerup', this.onEraserUp);
                         this._batch = UndoManager.StartBatch('collectionErase');
-                        e.stopPropagation();
-                        e.preventDefault();
+                        setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
                         break;
                     case InkTool.None:
                         if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
-                            document.addEventListener('pointermove', this.onPointerMove);
-                            document.addEventListener('pointerup', this.onPointerUp);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
                         }
                         break;
                 }
@@ -698,23 +695,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     @action
     onEraserUp = (e: PointerEvent): void => {
-        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
-            document.removeEventListener('pointermove', this.onEraserMove);
-            document.removeEventListener('pointerup', this.onEraserUp);
-            this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
-            this._deleteList = [];
-            this._batch?.end();
-        }
-    };
-
-    @action
-    onPointerUp = (e: PointerEvent): void => {
-        if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
-            document.removeEventListener('pointermove', this.onPointerMove);
-            document.removeEventListener('pointerup', this.onPointerUp);
-            this.removeMoveListeners();
-            this.removeEndListeners();
-        }
+        this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+        this._deleteList = [];
+        this._batch?.end();
     };
 
     onClick = (e: React.MouseEvent) => {
@@ -752,46 +735,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      *  However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
      */
     @action
-    onEraserMove = (e: PointerEvent) => {
+    onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
         const currPoint = { X: e.clientX, Y: e.clientY };
-        this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => {
+        this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
             if (!this._deleteList.includes(intersect.inkView)) {
                 this._deleteList.push(intersect.inkView);
                 SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || '1');
                 SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black');
                 // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
-                !e.shiftKey &&
+                if (!e.shiftKey) {
                     this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
                         GestureOverlay.Instance.dispatchGesture(
                             GestureUtils.Gestures.Stroke,
                             segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
                         )
                     );
+                }
                 // Lower ink opacity to give the user a visual indicator of deletion.
                 intersect.inkView.layoutDoc.opacity = 0.5;
+                intersect.inkView.layoutDoc.dontIntersect = true;
             }
         });
-        this._lastX = currPoint.X;
-        this._lastY = currPoint.Y;
-
-        e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-        e.preventDefault();
+        return false;
     };
 
     @action
-    onPointerMove = (e: PointerEvent): void => {
-        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
+    onPointerMove = (e: PointerEvent): boolean => {
+        if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false;
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
             Doc.ActiveTool = InkTool.None;
             if (this.props.isContentActive(true)) e.stopPropagation();
         } else if (!e.cancelBubble) {
             if (this.tryDragCluster(e, this._hitCluster)) {
-                document.removeEventListener('pointermove', this.onPointerMove);
-                document.removeEventListener('pointerup', this.onPointerUp);
+                return true;
             } else this.pan(e);
-            e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
-            e.preventDefault();
         }
+        return false;
     };
 
     /**
@@ -801,6 +780,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
         const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
         const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
+
         return this.childDocs
             .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
             .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
@@ -888,7 +868,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const tVals: number[] = [];
         // Iterating through all ink strokes in the current freeform collection.
         this.childDocs
-            .filter(doc => doc.type === DocumentType.INK)
+            .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
             .forEach(doc => {
                 const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
                 const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
@@ -919,7 +899,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
                         e.preventDefault();
                         document.removeEventListener('pointermove', this.onPointerMove);
-                        document.removeEventListener('pointerup', this.onPointerUp);
                         return;
                     }
                     // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected'
@@ -967,13 +946,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
                         // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
 
-                        if (!this._pullDirection) {
-                            // if we are not bezel movement
-                            this.pan({ clientX: centerX, clientY: centerY });
-                        } else {
-                            this._pullCoords = [centerX, centerY];
-                        }
-
                         this._lastX = centerX;
                         this._lastY = centerY;
                     }
@@ -998,24 +970,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
                 this._lastX = centerX;
                 this._lastY = centerY;
-                const screenBox = this._mainCont?.getBoundingClientRect();
-
-                // determine if we are using a bezel movement
-                if (screenBox) {
-                    if (screenBox.right - centerX < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'right';
-                    } else if (centerX - screenBox.left < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'left';
-                    } else if (screenBox.bottom - centerY < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'bottom';
-                    } else if (centerY - screenBox.top < 100) {
-                        this._pullCoords = [centerX, centerY];
-                        this._pullDirection = 'top';
-                    }
-                }
 
                 this.removeMoveListeners();
                 this.addMoveListeners();
@@ -1027,19 +981,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     cleanUpInteractions = () => {
-        switch (this._pullDirection) {
-            case 'left':
-            case 'right':
-            case 'top':
-            case 'bottom':
-                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: 'New Collection' }), this._pullDirection);
-        }
-
-        this._pullDirection = '';
-        this._pullCoords = [0, 0];
-
-        document.removeEventListener('pointermove', this.onPointerMove);
-        document.removeEventListener('pointerup', this.onPointerUp);
         this.removeMoveListeners();
         this.removeEndListeners();
     };
@@ -1380,7 +1321,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const { z, color, zIndex } = params.pair.layout;
         const { x, y, opacity } =
             this.Document._currentFrame === undefined
-                ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) }
+                ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.childOpacity ? this.props.childOpacity() : this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) }
                 : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame));
         return {
             x: NumCast(x),
@@ -1915,7 +1856,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 getContainerTransform={this.getContainerTransform}
                 getTransform={this.getTransform}
                 isAnnotationOverlay={this.isAnnotationOverlay}>
-                <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}>
+                <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}>
                     {this.layoutDoc._backgroundGridShow ? (
                         <div>
                             <CollectionFreeFormBackgroundGrid // bcz : UGHH  don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!?
@@ -1996,15 +1937,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 {this._firstRender ? this.placeholder : this.marqueeView}
                 {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
 
-                <div
-                    className={'pullpane-indicator'}
-                    style={{
-                        display: this._pullDirection ? 'block' : 'none',
-                        top: clientRect ? (this._pullDirection === 'bottom' ? this._pullCoords[1] - clientRect.y : 0) : 'auto',
-                        left: clientRect ? (this._pullDirection === 'right' ? this._pullCoords[0] - clientRect.x : 0) : 'auto',
-                        width: clientRect ? (this._pullDirection === 'left' ? this._pullCoords[0] - clientRect.left : this._pullDirection === 'right' ? clientRect.right - this._pullCoords[0] : clientRect.width) : 0,
-                        height: clientRect ? (this._pullDirection === 'top' ? this._pullCoords[1] - clientRect.top : this._pullDirection === 'bottom' ? clientRect.bottom - this._pullCoords[1] : clientRect.height) : 0,
-                    }}></div>
                 {
                     // uncomment to show snap lines
                     <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 78f351f4f..e19e2d525 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable, trace } from 'mobx';
+import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { List } from '../../../fields/List';
@@ -6,13 +6,12 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DashColor, numberRange, OmitKeys } from '../../../Utils';
+import { numberRange } from '../../../Utils';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { Transform } from '../../util/Transform';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { DocComponent } from '../DocComponent';
-import { InkingStroke } from '../InkingStroke';
 import { StyleProp } from '../StyleProvider';
 import './CollectionFreeFormDocumentView.scss';
 import { DocumentView, DocumentViewProps } from './DocumentView';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bcc55eab4..172adcafe 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -970,7 +970,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     collectionFilters = () => StrListCast(this.props.Document._docFilters);
     collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
     @computed get showFilterIcon() {
-        return this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? 'inheritsFilter' : undefined;
+        return this.collectionFilters().length || this.collectionRangeDocFilters().length
+            ? 'hasFilter'
+            : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length
+            ? 'inheritsFilter'
+            : undefined;
     }
     rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
     panelHeight = () => this.props.PanelHeight() - this.headerMargin;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index a3d501153..0ff15f93b 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -831,16 +831,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // stretches vertically or horizontally depending on video orientation so video fits full screen
     fullScreenSize() {
         if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
-            return { height: '100%' };
-        } else {
-            return { width: '100%' };
+            //prettier-ignore
+            return ({ height: '100%' });
         }
+        //prettier-ignore
+        return ({ width: '100%' });
     }
 
     // for zoom slider, sets timeline waveform zoom
-    zoom = (zoom: number) => {
-        this.timeline?.setZoom(zoom);
-    };
+    zoom = (zoom: number) => this.timeline?.setZoom(zoom);
 
     // plays link
     playLink = (doc: Doc) => {
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index d8dd074a5..85986ff27 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,9 +1,11 @@
-@import "../global/globalCssVariables.scss";
-
+@import '../global/globalCssVariables.scss';
 
 .webBox {
     height: 100%;
-    position: relative;
+    width: 100%;
+    top: 0;
+    left: 0;
+    position: absolute;
     display: flex;
 
     .webBox-sideResizer {
@@ -84,7 +86,6 @@
             background: none;
         }
 
-
         .webBox-overlayCont {
             position: absolute;
             width: calc(100% - 40px);
@@ -95,7 +96,7 @@
             justify-content: center;
             align-items: center;
             overflow: hidden;
-            transition: left .5s;
+            transition: left 0.5s;
             pointer-events: all;
 
             .webBox-searchBar {
@@ -158,7 +159,7 @@
             left: 0;
             cursor: text;
             padding: 15px;
-            height: 100%
+            height: 100%;
         }
 
         .webBox-cont {
@@ -235,7 +236,7 @@
         height: 25px;
         align-items: center;
 
-        >svg {
+        > svg {
             margin: auto;
         }
     }
@@ -257,4 +258,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index ca9f363c1..6c2e42f86 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as WebRequest from 'web-request';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -35,6 +35,7 @@ import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
 import './WebBox.scss';
 import React = require('react');
+import { DragManager } from '../../util/DragManager';
 const { CreateImage } = require('./WebBoxRenderer');
 const _global = (window /* browser */ || global) /* node */ as any;
 const htmlToText = require('html-to-text');
@@ -818,6 +819,56 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
     }
 
+    @computed get webpage() {
+        const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+        const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
+        const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
+        const renderAnnotations = (docFilters?: () => string[]) => (
+            <CollectionFreeFormView
+                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                renderDepth={this.props.renderDepth + 1}
+                isAnnotationOverlay={true}
+                fieldKey={this.annotationKey}
+                CollectionView={undefined}
+                setPreviewCursor={this.setPreviewCursor}
+                PanelWidth={this.panelWidth}
+                PanelHeight={this.panelHeight}
+                ScreenToLocalTransform={this.scrollXf}
+                NativeDimScaling={returnOne}
+                dropAction={'alias'}
+                docFilters={docFilters}
+                select={emptyFunction}
+                bringToFront={emptyFunction}
+                styleProvider={this.childStyleProvider}
+                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                removeDocument={this.removeDocument}
+                moveDocument={this.moveDocument}
+                addDocument={this.addDocument}
+                childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
+                pointerEvents={this.annotationPointerEvents}
+            />
+        );
+        return (
+            <div
+                className={'webBox-outerContent'}
+                ref={this._outerRef}
+                style={{
+                    height: `${100 / scale}%`,
+                    pointerEvents,
+                }}
+                onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe
+                onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
+                onPointerDown={this.onMarqueeDown}>
+                <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}>
+                    {this.content}
+                    {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>}
+                    {renderAnnotations(this.opaqueFilter)}
+                    {this.annotationLayer}
+                </div>
+            </div>
+        );
+    }
+
     @computed get searchUI() {
         return (
             <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}>
@@ -859,9 +910,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
     scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
-    basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')];
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
-    opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+    opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (doc.textInlineAnnotations) return 'none';
@@ -871,37 +921,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
     annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() ? 'all' : 'none');
     render() {
-        const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
         const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+        const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
-        const renderAnnotations = (docFilters?: () => string[]) => (
-            <CollectionFreeFormView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                renderDepth={this.props.renderDepth + 1}
-                isAnnotationOverlay={true}
-                fieldKey={this.annotationKey}
-                CollectionView={undefined}
-                setPreviewCursor={this.setPreviewCursor}
-                PanelWidth={this.panelWidth}
-                PanelHeight={this.panelHeight}
-                ScreenToLocalTransform={this.scrollXf}
-                NativeDimScaling={returnOne}
-                dropAction={'alias'}
-                docFilters={docFilters || this.basicFilter}
-                dontRenderDocuments={docFilters ? false : true}
-                select={emptyFunction}
-                bringToFront={emptyFunction}
-                whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                removeDocument={this.removeDocument}
-                moveDocument={this.moveDocument}
-                addDocument={this.addDocument}
-                styleProvider={this.childStyleProvider}
-                childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
-                pointerEvents={this.annotationPointerEvents}
-            />
-        );
         return (
-            <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? 'none' : undefined }}>
+            <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}>
                 <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
                 <div
                     className="webBox-container"
@@ -911,27 +935,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         pointerEvents,
                     }}
                     onContextMenu={this.specificContextMenu}>
-                    <div
-                        className={'webBox-outerContent'}
-                        ref={this._outerRef}
-                        style={{
-                            height: `${100 / scale}%`,
-                            pointerEvents,
-                        }}
-                        onWheel={e => {
-                            e.stopPropagation();
-                            e.preventDefault();
-                        }} // block wheel events from propagating since they're handled by the iframe
-                        onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
-                        onPointerDown={this.onMarqueeDown}>
-                        <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}>
-                            {this.content}
-                            <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
-                            {renderAnnotations(this.opaqueFilter)}
-                            {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
-                            {this.annotationLayer}
-                        </div>
-                    </div>
+                    {this.webpage}
                     {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
                         <div style={{ transformOrigin: 'top left', transform: `scale(${1 / scale})` }}>
                             <MarqueeAnnotator
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 2c83082b7..a45edfbca 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -9,6 +9,7 @@ import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
+import { DragManager } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SharingManager } from '../../util/SharingManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -487,9 +488,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
     overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1));
     panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
     panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
-    basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')];
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
-    opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+    opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (doc.textInlineAnnotations) return 'none';
@@ -498,56 +498,43 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this.props.styleProvider?.(doc, props, property);
     };
 
-    renderAnnotations = (docFilters?: () => string[], dontRender?: boolean) => (
-        <CollectionFreeFormView
-            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-            isAnnotationOverlay={true}
-            fieldKey={this.props.fieldKey + '-annotations'}
-            setPreviewCursor={this.setPreviewCursor}
-            PanelHeight={this.panelHeight}
-            PanelWidth={this.panelWidth}
-            dropAction={'alias'}
-            select={emptyFunction}
-            bringToFront={emptyFunction}
-            docFilters={docFilters || this.basicFilter}
-            styleProvider={this.childStyleProvider}
-            dontRenderDocuments={dontRender}
-            CollectionView={undefined}
-            ScreenToLocalTransform={this.overlayTransform}
-            renderDepth={this.props.renderDepth + 1}
-        />
+    renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
+        <div
+            className="pdfViewerDash-overlay"
+            style={{
+                mixBlendMode: mixBlendMode,
+                display: display,
+                transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
+            }}>
+            <CollectionFreeFormView
+                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                renderDepth={this.props.renderDepth + 1}
+                isAnnotationOverlay={true}
+                fieldKey={this.props.fieldKey + '-annotations'}
+                CollectionView={undefined}
+                setPreviewCursor={this.setPreviewCursor}
+                PanelHeight={this.panelHeight}
+                PanelWidth={this.panelWidth}
+                ScreenToLocalTransform={this.overlayTransform}
+                dropAction={'alias'}
+                docFilters={docFilters}
+                select={emptyFunction}
+                bringToFront={emptyFunction}
+                styleProvider={this.childStyleProvider}
+            />
+        </div>
     );
     @computed get overlayTransparentAnnotations() {
-        return this.renderAnnotations(this.transparentFilter, false);
+        return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined);
     }
     @computed get overlayOpaqueAnnotations() {
-        return this.renderAnnotations(this.opaqueFilter, false);
-    }
-    @computed get overlayClickableAnnotations() {
-        return <div style={{ height: NumCast(this.props.rootDoc.scrollHeight) }}>{this.renderAnnotations(undefined, true)}</div>;
+        return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined);
     }
     @computed get overlayLayer() {
         return (
             <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}>
-                <div
-                    className="pdfViewerDash-overlay"
-                    style={{
-                        pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none',
-                        mixBlendMode: 'multiply',
-                        transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
-                    }}>
-                    {this.overlayTransparentAnnotations}
-                </div>
-                <div
-                    className="pdfViewerDash-overlay"
-                    style={{
-                        pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none',
-                        mixBlendMode: this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined,
-                        transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
-                    }}>
-                    {this.overlayOpaqueAnnotations}
-                    {this.overlayClickableAnnotations}
-                </div>
+                {this.overlayTransparentAnnotations}
+                {this.overlayOpaqueAnnotations}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 94c38310c6b54d93e907007f20ba032d12697ca0 Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Thu, 18 Aug 2022 12:33:34 -0400
Subject: fixed sizing bug

---
 src/client/documents/Documents.ts                  | 14 +++++--------
 src/client/views/collections/CollectionSubView.tsx |  2 ++
 src/client/views/nodes/LoadingBox.tsx              | 24 ++++++++++++++++++----
 3 files changed, 27 insertions(+), 13 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e54fe16de..f3ab7c788 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -827,9 +827,12 @@ export namespace Docs {
             viewFirstProps.author = Doc.CurrentUserEmail;
             let viewDoc: Doc;
             if (placeholderDoc) {
+                placeholderDoc._height = Number(options._height);
+                placeholderDoc._width = Number(options._width);
                 viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
+            } else {
+                viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
             }
-            viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
             Doc.assign(viewDoc, viewProps, true, true);
             ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
 
@@ -901,7 +904,7 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
         }
 
-        export const filesToDocs = new Map<Doc, File>();
+        export const filesToDocs = new Map<Doc, File | string>();
 
         export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) {
             const fileName = typeof file == 'string' ? file : file.name;
@@ -1854,13 +1857,6 @@ export namespace DocUtils {
         });
     }
 
-    export function generatePlaceHolder(file: File, options: DocumentOptions) {
-        return Docs.Create.LoadingDocument(file, options);
-        // placeholder.file = file
-        // TODO: nda - modify loading doc so it only takes in options
-        // Docs.Create.LoadingDocument(options, )
-    }
-
     // copies the specified drag factory document
     export function copyDragFactory(dragFactory: Doc) {
         if (!dragFactory) return undefined;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 30467efa0..fd2c722d2 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -470,6 +470,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 // generatedDocuments.push(Docs.Create.LoadingDocument(files, options));
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
+                Docs.Create.filesToDocs.set(loading, files);
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
@@ -477,6 +478,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
                         // now that there is a doc do whatever slowload was going to do with that file
+                        Docs.Create.filesToDocs.set(loading, file);
                         DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
                     })
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 96620aff9..249461b67 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -5,6 +5,8 @@ import * as React from 'react';
 import './LoadingBox.scss';
 import ReactLoading from 'react-loading';
 import { StrCast } from '../../../fields/Types';
+import { computed, observable } from 'mobx';
+import { Docs } from '../../documents/Documents';
 
 @observer
 export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -12,6 +14,12 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(LoadingBox, fieldKey);
     }
 
+    @computed
+    private get isLoading() {
+        const file = Docs.Create.filesToDocs.get(this.rootDoc);
+        return file ? true : false;
+    }
+
     componentDidMount() {
         console.log(this.rootDoc);
         // const file = Docs.Create.filesToDocs.get(this.rootDoc);
@@ -44,10 +52,18 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     render() {
         return (
             <div className="loadingBoxContainer">
-                <p className="text">Loading:</p>
-                <br></br>
-                <p className="text">{StrCast(this.rootDoc.title)}</p>
-                <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />
+                {this.isLoading ? (
+                    <div>
+                        <p className="text">Loading:</p>
+                        <br></br>
+                        <span className="text">{StrCast(this.rootDoc.title)}</span>
+                        <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />
+                    </div>
+                ) : (
+                    <div>
+                        <span>Error Loading File: {StrCast(this.rootDoc.title)}</span>
+                    </div>
+                )}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From b7c4d65a3bf04ff7d2c10d93be282a1b0a4650b3 Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Fri, 19 Aug 2022 12:32:23 -0400
Subject: added comments, cleaned up code, could not get spinner to center

---
 src/client/documents/Documents.ts                  |  8 ++-
 src/client/views/collections/CollectionSubView.tsx |  4 +-
 src/client/views/nodes/LoadingBox.scss             | 21 +++++++
 src/client/views/nodes/LoadingBox.tsx              | 64 ++++++++++------------
 4 files changed, 58 insertions(+), 39 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f3ab7c788..9e140ccd1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -815,6 +815,7 @@ export namespace Docs {
             dataProps[fieldKey + '-annotations'] = new List<Doc>();
             dataProps[fieldKey + '-sidebar'] = new List<Doc>();
 
+            // users placeholderDoc as proto if it exists
             const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
 
             if (placeholderDoc) {
@@ -826,6 +827,7 @@ export namespace Docs {
             viewFirstProps['acl-Override'] = 'None';
             viewFirstProps.author = Doc.CurrentUserEmail;
             let viewDoc: Doc;
+            // determines whether viewDoc should be created using placeholder Doc or deafult
             if (placeholderDoc) {
                 placeholderDoc._height = Number(options._height);
                 placeholderDoc._width = Number(options._width);
@@ -904,13 +906,13 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
         }
 
-        export const filesToDocs = new Map<Doc, File | string>();
+        // Mapping of all loading docs to files, i.e. keeps track of files being uploaded in current session
+        export const docsToFiles = new Map<Doc, File | string>();
 
         export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) {
             const fileName = typeof file == 'string' ? file : file.name;
             options.title = fileName;
-            const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 150, ...options });
-            // filesToDocs.set(loading, file);
+            const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 200, ...options });
             return loading;
         }
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index fd2c722d2..eef485b95 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -470,7 +470,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 // generatedDocuments.push(Docs.Create.LoadingDocument(files, options));
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
-                Docs.Create.filesToDocs.set(loading, files);
+                Docs.Create.docsToFiles.set(loading, files);
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
@@ -478,7 +478,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
                         // now that there is a doc do whatever slowload was going to do with that file
-                        Docs.Create.filesToDocs.set(loading, file);
+                        Docs.Create.docsToFiles.set(loading, file);
                         DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
                     })
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index e8890cd82..f6912f547 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -6,7 +6,28 @@
     background-color: #fdfdfd;
 }
 
+.textContainer {
+    margin: 5px;
+}
+
+.textContainer {
+    justify-content: center;
+    align-content: center;
+}
+
+.textContainer,
 .text {
+    overflow: hidden;
     text-overflow: ellipsis;
+    max-width: 80%;
+    text-align: center;
+}
+
+.headerText {
+    text-align: center;
+    font-weight: bold;
+}
+
+.spinner {
     text-align: center;
 }
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 249461b67..ab8878fc1 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -5,46 +5,42 @@ import * as React from 'react';
 import './LoadingBox.scss';
 import ReactLoading from 'react-loading';
 import { StrCast } from '../../../fields/Types';
-import { computed, observable } from 'mobx';
+import { computed } from 'mobx';
 import { Docs } from '../../documents/Documents';
 
+/**
+ * LoadingBox Class represents a placeholder doc for documents that are currently
+ * being uploaded to the server and being fetched by the client. The LoadingBox doc is then used to
+ * generate the actual type of doc that is required once the document has been successfully uploaded.
+ *
+ * Design considerations:
+ * We are using the docToFiles map in Documents to keep track of all files being uploaded in one session of the client.
+ * If the file is not found we assume an error has occurred with the file upload, e.g. it has been interrupted by a client refresh
+ * or network issues. The docToFiles essentially gets reset everytime the page is refreshed.
+ *
+ * TODOs:
+ *  1) ability to query server to retrieve files that already exist if users upload duplicate files.
+ *  2) ability to restart upload if there is an error
+ *  3) detect network error and notify the user
+ *  4 )if file upload gets interrupted, it still gets uploaded to the server if there are no network interruptions which leads to unused space. this could be
+ *      handled with (1)
+ *
+ * @author naafiyan
+ */
 @observer
 export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(LoadingBox, fieldKey);
     }
 
-    @computed
-    private get isLoading() {
-        const file = Docs.Create.filesToDocs.get(this.rootDoc);
+    /**
+     * Returns true if file is uploading, false otherwise
+     */
+    @computed private get isLoading(): boolean {
+        const file = Docs.Create.docsToFiles.get(this.rootDoc);
         return file ? true : false;
     }
 
-    componentDidMount() {
-        console.log(this.rootDoc);
-        // const file = Docs.Create.filesToDocs.get(this.rootDoc);
-        // if (file) {
-        //     console.log('Got to file');
-        //     Docs.Create.filesToDocs.delete(this.rootDoc);
-        //     // now that there is a doc do whatever slowload was going to do with that file
-        //     if (typeof file === 'string') {
-        //         // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
-        //         // (await DocUtils.uploadYoutubeVideo(files, options)));
-        //     } else {
-        //         // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
-        //         DocUtils.uploadFileToDoc(file, {}, this.rootDoc);
-        //     }
-        // } else {
-        //     // check if file now exists on server or not
-        //     // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading)
-        //     // if it doesn't display an error message "upload failed"
-        // }
-        // query endpoints to:
-        // check if file now exists on server or not
-        // if it does we need to retreieve it and create the appropriate doc (rest of what uploadFileToDoc was doing minus uploading)
-        // if it doesn't display an error message "upload failed"
-    }
-
     constructor(props: any) {
         super(props);
     }
@@ -53,15 +49,15 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return (
             <div className="loadingBoxContainer">
                 {this.isLoading ? (
-                    <div>
-                        <p className="text">Loading:</p>
-                        <br></br>
+                    <div className="textContainer">
+                        <p className="headerText">Loading:</p>
                         <span className="text">{StrCast(this.rootDoc.title)}</span>
                         <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />
                     </div>
                 ) : (
-                    <div>
-                        <span>Error Loading File: {StrCast(this.rootDoc.title)}</span>
+                    <div className="textContainer">
+                        <p className="headerText">Error Loading File: </p>
+                        <span className="text">{StrCast(this.rootDoc.title)}</span>
                     </div>
                 )}
             </div>
-- 
cgit v1.2.3-70-g09d2


From 5b20224b59f4a6ebc02fbd0a68441bb241c68d63 Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Fri, 19 Aug 2022 14:37:32 -0400
Subject: cleaned up comments

---
 src/client/views/collections/CollectionSubView.tsx | 5 -----
 1 file changed, 5 deletions(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index d7d988c19..e70cfb13c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -460,18 +460,13 @@ export function CollectionSubView<X>(moreProps?: X) {
             // create placeholder docs
             // inside placeholder docs have some func that
 
-            // TODO: once loading thing is moved it should update the x and y of the file it is placeholder for
             let pileUpDoc = undefined;
-            // const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
             if (typeof files === 'string') {
-                // uploadYoutubeVideo and similar should return a placeholder, one for each placeholder
-                // generatedDocuments.push(Docs.Create.LoadingDocument(files, options));
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
                 Docs.Create.docsToFiles.set(loading, files);
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
-                // uploadFilesToDocs and similar should return a placeholder, one for each placeholder
                 generatedDocuments.push(
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
-- 
cgit v1.2.3-70-g09d2


From 7f2556568ab635274c483d102fa4555d12e14835 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Aug 2022 13:55:41 -0400
Subject: fixed loadingDocument issues with overwriting height with NaN and
 setting 'data' field unnecessarily and at the expense of being able to set it
 for replacement docs when using animation frames

---
 src/client/documents/Documents.ts                  | 34 ++++++++++------------
 src/client/views/GlobalKeyHandler.ts               |  9 +++---
 src/client/views/collections/CollectionSubView.tsx |  6 ++--
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  6 ++--
 src/client/views/nodes/LoadingBox.tsx              | 29 ++++--------------
 5 files changed, 30 insertions(+), 54 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9446eb70c..b22e16633 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -653,7 +653,7 @@ export namespace Docs {
             [
                 DocumentType.LOADING,
                 {
-                    layout: { view: LoadingBox, dataField: defaultDataKey },
+                    layout: { view: LoadingBox, dataField: '' },
                     options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
                 },
             ],
@@ -806,14 +806,15 @@ export namespace Docs {
             dataProps.isPrototype = true;
             dataProps.author = Doc.CurrentUserEmail;
             dataProps.creationDate = new DateField();
-            dataProps[`${fieldKey}-lastModified`] = new DateField();
-
-            dataProps[fieldKey] = data;
-
-            // so that the list of annotations is already initialised, prevents issues in addonly.
-            // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
-            dataProps[fieldKey + '-annotations'] = new List<Doc>();
-            dataProps[fieldKey + '-sidebar'] = new List<Doc>();
+            if (fieldKey) {
+                dataProps[`${fieldKey}-lastModified`] = new DateField();
+                dataProps[fieldKey] = data;
+
+                // so that the list of annotations is already initialised, prevents issues in addonly.
+                // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
+                dataProps[fieldKey + '-annotations'] = new List<Doc>();
+                dataProps[fieldKey + '-sidebar'] = new List<Doc>();
+            }
 
             // users placeholderDoc as proto if it exists
             const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
@@ -829,8 +830,8 @@ export namespace Docs {
             let viewDoc: Doc;
             // determines whether viewDoc should be created using placeholder Doc or default
             if (placeholderDoc) {
-                placeholderDoc._height = Number(options._height);
-                placeholderDoc._width = Number(options._width);
+                placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined;
+                placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined;
                 viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
             } else {
                 viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
@@ -846,6 +847,7 @@ export namespace Docs {
 
             updateCachedAcls(dataDoc);
             updateCachedAcls(viewDoc);
+
             return viewDoc;
         }
 
@@ -905,15 +907,8 @@ export namespace Docs {
         export function ColorDocument(options: DocumentOptions = {}) {
             return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
         }
-
-        // Mapping of all loading docs to files, i.e. keeps track of files being uploaded in current session
-        export const docsToFiles = new Map<Doc, File | string>();
-
         export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) {
-            const fileName = typeof file == 'string' ? file : file.name;
-            options.title = fileName;
-            const loading = InstanceFromProto(Prototypes.get(DocumentType.LOADING), fileName, { _height: 150, _width: 200, ...options });
-            return loading;
+            return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, '');
         }
 
         export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
@@ -1756,6 +1751,7 @@ export namespace DocUtils {
         const full = { ...options, _width: 400, title: name };
         const pathname = Utils.prepend(result.accessPaths.agnostic.client);
         const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc);
+        rootDoc && (rootDoc.isLoading = undefined);
         if (doc) {
             const proto = Doc.GetProto(doc);
             proto.text = result.rawText;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 88ce457c6..50518eec1 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -84,6 +84,7 @@ export class KeyManager {
     });
 
     private unmodified = action((keyname: string, e: KeyboardEvent) => {
+        const hasFffView = SelectionManager.Views().some(dv => dv.props.CollectionFreeFormDocumentView?.());
         switch (keyname) {
             case 'u':
                 if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
@@ -160,16 +161,16 @@ export class KeyManager {
                 break;
             case 'arrowleft':
                 UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge(-1, 0)), 'nudge left');
-                return { stopPropagation: true, preventDefault: true };
+                return { stopPropagation: hasFffView, preventDefault: hasFffView };
             case 'arrowright':
                 UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(1, 0)), 'nudge right');
-                return { stopPropagation: true, preventDefault: true };
+                return { stopPropagation: hasFffView, preventDefault: hasFffView };
             case 'arrowup':
                 UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, -1)), 'nudge up');
-                return { stopPropagation: true, preventDefault: true };
+                return { stopPropagation: hasFffView, preventDefault: hasFffView };
             case 'arrowdown':
                 UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 1)), 'nudge down');
-                return { stopPropagation: true, preventDefault: true };
+                return { stopPropagation: hasFffView, preventDefault: hasFffView };
         }
 
         return {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e70cfb13c..4227955ef 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -464,14 +464,13 @@ export function CollectionSubView<X>(moreProps?: X) {
             if (typeof files === 'string') {
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
-                Docs.Create.docsToFiles.set(loading, files);
+                loading.isLoading = true;
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 generatedDocuments.push(
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
-                        // now that there is a doc do whatever slowload was going to do with that file
-                        Docs.Create.docsToFiles.set(loading, file);
+                        loading.isLoading = true;
                         DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
                     })
@@ -516,5 +515,4 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
-import { OverlayView } from '../OverlayView';
 import { CollectionView, CollectionViewProps } from './CollectionView';
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index cfa4bb160..7907470dc 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -35,7 +35,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
     public static animFields: { key: string; val?: number }[] = [{ key: '_height' }, { key: '_width' }, { key: 'x' }, { key: 'y' }, { key: '_scrollTop' }, { key: 'opacity', val: 1 }, { key: 'viewScale', val: 1 }, { key: 'panX' }, { key: 'panY' }]; // fields that are configured to be animatable using animation frames
     public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames
-    public static animDataFields = ['data', 'text']; // fields that are configured to be animatable using animation frames
+    public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
     @observable _animPos: number[] | undefined = undefined;
     @observable _contentView: DocumentView | undefined | null;
     get displayName() {
@@ -121,7 +121,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
                     findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
                 });
-                CollectionFreeFormDocumentView.animDataFields.forEach(val => {
+                CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
                     const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
                     findexed?.length <= timecode + 1 && findexed.push(undefined as any);
                 });
@@ -173,7 +173,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             }
             CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val)));
             CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
-            CollectionFreeFormDocumentView.animDataFields.forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
             const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so
             doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
             targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 90bf90095..f3243f6cd 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -35,33 +35,14 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(LoadingBox, fieldKey);
     }
 
-    /**
-     * Returns true if file is uploading, false otherwise
-     */
-    @computed private get isLoading(): boolean {
-        const file = Docs.Create.docsToFiles.get(this.rootDoc);
-        return file ? true : false;
-    }
-
-    constructor(props: any) {
-        super(props);
-    }
-
     render() {
         return (
             <div className="loadingBoxContainer">
-                {this.isLoading ? (
-                    <div className="textContainer">
-                        <p className="headerText">Loading:</p>
-                        <span className="text">{StrCast(this.rootDoc.title)}</span>
-                        <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />
-                    </div>
-                ) : (
-                    <div className="textContainer">
-                        <p className="headerText">Error Loading File: </p>
-                        <span className="text">{StrCast(this.rootDoc.title)}</span>
-                    </div>
-                )}
+                <div className="textContainer">
+                    <p className="headerText">{this.rootDoc.isLoading ? 'Loading:' : 'Error Loading File:'}</p>
+                    <span className="text">{StrCast(this.rootDoc.title)}</span>
+                    {!this.rootDoc.isLoading ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
+                </div>
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 945c0cd12fb2a792d10800f81b52cbac8aa12a41 Mon Sep 17 00:00:00 2001
From: mehekj <mehek.jethani@gmail.com>
Date: Tue, 20 Sep 2022 11:52:44 -0400
Subject: display error instead of infinitely loading after refresh

---
 src/client/documents/Documents.ts                  |  4 +++-
 src/client/views/collections/CollectionSubView.tsx |  2 ++
 src/client/views/nodes/LoadingBox.tsx              |  9 +++++++++
 src/fields/Doc.ts                                  | 22 ++++++++++++++++++++++
 4 files changed, 36 insertions(+), 1 deletion(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 14e6fe5bb..4f652b6e4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1758,7 +1758,7 @@ export namespace DocUtils {
         const full = { ...options, _width: 400, title: name };
         const pathname = Utils.prepend(result.accessPaths.agnostic.client);
         const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc);
-        rootDoc && (rootDoc.isLoading = undefined);
+        rootDoc && (rootDoc.isLoading = undefined) && Doc.removeCurrentlyLoading(rootDoc);
         if (doc) {
             const proto = Doc.GetProto(doc);
             proto.text = result.rawText;
@@ -1840,6 +1840,7 @@ export namespace DocUtils {
                 if (overwriteDoc) {
                     overwriteDoc.isLoading = false;
                     overwriteDoc.errorMessage = (result as any).message;
+                    Doc.removeCurrentlyLoading(overwriteDoc);
                 }
             } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
         });
@@ -1868,6 +1869,7 @@ export namespace DocUtils {
                 if (overwriteDoc) {
                     overwriteDoc.isLoading = false;
                     overwriteDoc.errorMessage = (result as any).message;
+                    Doc.removeCurrentlyLoading(overwriteDoc);
                 }
             } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
         });
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7fc1a800b..3ae965af0 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -465,12 +465,14 @@ export function CollectionSubView<X>(moreProps?: X) {
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
                 loading.isLoading = true;
+                Doc.addCurrentlyLoading(loading);
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 generatedDocuments.push(
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
                         loading.isLoading = true;
+                        Doc.addCurrentlyLoading(loading);
                         DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
                     })
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index acf728ebb..220cd0880 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -1,6 +1,8 @@
+import { observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
+import { Doc } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
@@ -33,6 +35,13 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(LoadingBox, fieldKey);
     }
 
+    componentDidMount() {
+        if (!Doc.CurrentlyLoading || !Doc.CurrentlyLoading.includes(this.rootDoc)) {
+            this.rootDoc.isLoading = false;
+            this.rootDoc.errorMessage = 'Upload was interrupted, please try again';
+        }
+    }
+
     render() {
         return (
             <div className="loadingBoxContainer" style={{ background: this.rootDoc.isLoading ? '' : 'red' }}>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index cf505c45b..74a3d8cf2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -149,6 +149,28 @@ export class Doc extends RefField {
     public static set MainDocId(id: string | undefined) {
         this.mainDocId = id;
     }
+
+    @observable public static CurrentlyLoading: Doc[];
+    // removes from currently loading display
+    @action
+    public static removeCurrentlyLoading(doc: Doc) {
+        if (Doc.CurrentlyLoading) {
+            const index = Doc.CurrentlyLoading.indexOf(doc);
+            index !== -1 && Doc.CurrentlyLoading.splice(index, 1);
+        }
+    }
+
+    // adds doc to currently loading display
+    @action
+    public static addCurrentlyLoading(doc: Doc) {
+        if (!Doc.CurrentlyLoading) {
+            Doc.CurrentlyLoading = [];
+        }
+        if (Doc.CurrentlyLoading.indexOf(doc) === -1) {
+            Doc.CurrentlyLoading.push(doc);
+        }
+    }
+
     @observable public static GuestDashboard: Doc | undefined;
     @observable public static GuestTarget: Doc | undefined;
     @observable public static GuestMobile: Doc | undefined;
-- 
cgit v1.2.3-70-g09d2


From 48e0885b405cbfe728b1eda78efc19e15a104426 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 22 Sep 2022 16:00:17 -0400
Subject: fixed error messages on loading to not generate temporary error
 message when there is no error.

---
 src/client/documents/Documents.ts                  | 22 ++++++----------------
 src/client/views/collections/CollectionSubView.tsx |  2 --
 src/client/views/nodes/LoadingBox.tsx              | 12 +++++-------
 3 files changed, 11 insertions(+), 25 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 029c3033d..f046af684 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -908,7 +908,7 @@ export namespace Docs {
         export function ColorDocument(options: DocumentOptions = {}) {
             return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
         }
-        export function LoadingDocument(file: File | string, options: DocumentOptions, ytString?: string) {
+        export function LoadingDocument(file: File | string, options: DocumentOptions) {
             return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, '');
         }
 
@@ -1758,7 +1758,6 @@ export namespace DocUtils {
         const full = { ...options, _width: 400, title: name };
         const pathname = Utils.prepend(result.accessPaths.agnostic.client);
         const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc);
-        rootDoc && (rootDoc.isLoading = undefined) && Doc.removeCurrentlyLoading(rootDoc);
         if (doc) {
             const proto = Doc.GetProto(doc);
             proto.text = result.rawText;
@@ -1790,6 +1789,9 @@ export namespace DocUtils {
             if (Upload.isVideoInformation(result)) {
                 proto['data-duration'] = result.duration;
             }
+            if (rootDoc) {
+                Doc.removeCurrentlyLoading(rootDoc);
+            }
             generatedDocuments.push(doc);
         }
     }
@@ -1818,17 +1820,6 @@ export namespace DocUtils {
         return tbox;
     }
 
-    export async function uploadYoutubeVideo(videoId: string, options: DocumentOptions) {
-        const generatedDocuments: Doc[] = [];
-        for (const {
-            source: { name, type },
-            result,
-        } of await Networking.UploadYoutubeToServer(videoId)) {
-            name && processFileupload(generatedDocuments, name, type, result, options);
-        }
-        return generatedDocuments;
-    }
-
     export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) {
         const generatedDocuments: Doc[] = [];
         Networking.UploadYoutubeToServer(videoId).then(upfiles => {
@@ -1839,7 +1830,7 @@ export namespace DocUtils {
             if ((result as any).message) {
                 if (overwriteDoc) {
                     overwriteDoc.isLoading = false;
-                    overwriteDoc.errorMessage = (result as any).message;
+                    overwriteDoc.loadingError = (result as any).message;
                     Doc.removeCurrentlyLoading(overwriteDoc);
                 }
             } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
@@ -1867,8 +1858,7 @@ export namespace DocUtils {
             } = upfiles.lastElement() ?? { source: { name: '', type: '' }, result: { message: 'upload failed' } };
             if ((result as any).message) {
                 if (overwriteDoc) {
-                    overwriteDoc.isLoading = false;
-                    overwriteDoc.errorMessage = (result as any).message;
+                    overwriteDoc.loadingError = (result as any).message;
                     Doc.removeCurrentlyLoading(overwriteDoc);
                 }
             } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 3ae965af0..30759b766 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -464,14 +464,12 @@ export function CollectionSubView<X>(moreProps?: X) {
             if (typeof files === 'string') {
                 const loading = Docs.Create.LoadingDocument(files, options);
                 generatedDocuments.push(loading);
-                loading.isLoading = true;
                 Doc.addCurrentlyLoading(loading);
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 generatedDocuments.push(
                     ...files.map(file => {
                         const loading = Docs.Create.LoadingDocument(file, options);
-                        loading.isLoading = true;
                         Doc.addCurrentlyLoading(loading);
                         DocUtils.uploadFileToDoc(file, {}, loading);
                         return loading;
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 220cd0880..53390328f 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -1,4 +1,3 @@
-import { observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
@@ -36,19 +35,18 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
 
     componentDidMount() {
-        if (!Doc.CurrentlyLoading || !Doc.CurrentlyLoading.includes(this.rootDoc)) {
-            this.rootDoc.isLoading = false;
-            this.rootDoc.errorMessage = 'Upload was interrupted, please try again';
+        if (!Doc.CurrentlyLoading?.includes(this.rootDoc)) {
+            this.rootDoc.loadingError = 'Upload interrupted, please try again';
         }
     }
 
     render() {
         return (
-            <div className="loadingBoxContainer" style={{ background: this.rootDoc.isLoading ? '' : 'red' }}>
+            <div className="loadingBoxContainer" style={{ background: !this.rootDoc.loadingError ? '' : 'red' }}>
                 <div className="textContainer">
-                    <p className="headerText">{this.rootDoc.isLoading ? 'Loading (can take several minutes):' : StrCast(this.rootDoc.errorMessage, 'Error Loading File:')}</p>
+                    <p className="headerText">{StrCast(this.rootDoc.loadingError, 'Loading (can take several minutes):')}</p>
                     <span className="text">{StrCast(this.rootDoc.title)}</span>
-                    {!this.rootDoc.isLoading ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
+                    {this.rootDoc.loadingError ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 56116231c4c0aa78d54a9ed4c1f167514596953c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Nov 2022 23:27:05 -0500
Subject: added presEffects to link anchors

---
 src/client/util/DocumentManager.ts                 |  2 +-
 src/client/views/MainView.tsx                      |  1 +
 .../views/collections/CollectionDockingView.tsx    |  7 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/linking/LinkEditor.tsx            | 74 ++++++++++++++++++-
 src/client/views/nodes/DocumentView.scss           |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/trails/PresBox.tsx          | 86 ++++++++++++----------
 src/fields/Doc.ts                                  |  7 +-
 9 files changed, 136 insertions(+), 49 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index a60c1ed6b..00f6bc40a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -233,7 +233,7 @@ export class DocumentManager {
             }
         }
         if (focusView) {
-            !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
+            !noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
             if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
                 focusView.focus(originalTarget ?? targetDoc, {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 987bfc23d..392b4eeeb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -484,6 +484,7 @@ export class MainView extends React.Component {
     }
 
     globalPointerDown = action((e: PointerEvent) => {
+        runInAction(() => (Doc.HighlightBrush.linkFollowEffect = undefined));
         AudioBox.Enabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets.length) {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 92319d080..434466505 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -452,7 +452,7 @@ export class CollectionDockingView extends CollectionSubView() {
                   .map(id => DocServer.GetCachedRefField(id))
                   .filter(f => f)
                   .map(f => f as Doc);
-        const changesMade = this.props.Document.dockcingConfig !== json;
+        const changesMade = this.props.Document.dockingConfig !== json;
         if (changesMade && !this._flush) {
             UndoManager.RunInBatch(() => {
                 this.props.Document.dockingConfig = json;
@@ -507,7 +507,12 @@ export class CollectionDockingView extends CollectionSubView() {
                 action(() => {
                     //if (confirm('really close this?')) {
                     if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) {
+                        const batch = UndoManager.StartBatch('close stack');
                         stack.remove();
+                        setTimeout(() => {
+                            this.stateChanged();
+                            batch.end();
+                        });
                     } else {
                         alert('cant delete the last stack');
                     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7bc273d7d..b66dc0aa2 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -111,7 +111,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
             }
 
-            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc);
+            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && !d.unrendered).map(d => d as Doc);
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
 
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 3e8867c50..d90a91ab7 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -3,13 +3,14 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc';
-import { DateCast, StrCast, Cast, BoolCast } from '../../../fields/Types';
+import { DateCast, StrCast, Cast, BoolCast, DocCast, NumCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import './LinkEditor.scss';
 import { LinkRelationshipSearch } from './LinkRelationshipSearch';
 import React = require('react');
 import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { PresBox, PresEffect } from '../nodes/trails';
 
 interface LinkEditorProps {
     sourceDoc: Doc;
@@ -24,6 +25,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom);
     @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio);
     @observable openDropdown: boolean = false;
+    @observable openEffectDropdown: boolean = false;
     @observable private buttonColor: string = '';
     @observable private relationshipButtonColor: string = '';
     @observable private relationshipSearchVisibility: string = 'none';
@@ -295,6 +297,61 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         );
     }
 
+    @computed get sourceAnchor() {
+        const ldoc = this.props.linkDoc;
+        if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) {
+            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1);
+            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2);
+        }
+        return this.props.sourceDoc;
+    }
+    @action
+    changeEffectDropdown = () => {
+        this.openEffectDropdown = !this.openEffectDropdown;
+    };
+
+    @undoBatch
+    changeEffect = action((follow: string) => {
+        this.openEffectDropdown = false;
+        this.sourceAnchor.presEffect = follow;
+    });
+
+    @computed
+    get effectDropdown() {
+        return (
+            <div className="linkEditor-followingDropdown">
+                <div className="linkEditor-followingDropdown-label">Transition Effect:</div>
+                <div className="linkEditor-followingDropdown-dropdown">
+                    <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}>
+                        {StrCast(this.sourceAnchor.presEffect, 'default')}
+                        <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openEffectDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
+                    </div>
+                    <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openEffectDropdown ? '' : 'none' }}>
+                        {[
+                            PresEffect.None,
+                            PresEffect.Zoom,
+                            PresEffect.Lightspeed,
+                            PresEffect.Fade,
+                            PresEffect.Flip,
+                            PresEffect.Rotate,
+                            PresEffect.Bounce,
+                            PresEffect.Roll,
+                            PresEffect.Left,
+                            PresEffect.Right,
+                            PresEffect.Center,
+                            PresEffect.Top,
+                            PresEffect.Bottom,
+                        ].map(effect => (
+                            <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeEffect(effect.toString())}>
+                                {effect.toString()}
+                            </div>
+                        ))}
+                    </div>
+                </div>
+            </div>
+        );
+    }
+
     autoMove = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
     };
@@ -348,7 +405,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                         </>
                     ) : null}
                 </div>
-
                 {this.editDescription}
                 {this.editRelationship}
                 {this.editZoomFollow}
@@ -390,6 +446,20 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </Tooltip>
                 </div>
                 {this.followingDropdown}
+                {this.effectDropdown}
+                {PresBox.inputter('0.1', '0.1', '10', NumCast(this.sourceAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.sourceAnchor.presTransition = timeInMS)))}
+                <div
+                    className={'slider-headers'}
+                    style={{
+                        display: 'grid',
+                        justifyContent: 'space-between',
+                        width: '100%',
+                        gridTemplateColumns: 'auto auto auto',
+                    }}>
+                    <div className="slider-text">Fast</div>
+                    <div className="slider-text">Medium</div>
+                    <div className="slider-text">Slow</div>
+                </div>{' '}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 01188d3fa..abf6e37ab 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -28,7 +28,7 @@
     transition: outline 0.3s linear;
 
     // background: $white; //overflow: hidden;
-    transform-origin: left top;
+    transform-origin: center;
 
     &.minimized {
         width: 30px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dc468cf89..fb07f1033 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
-import { observer } from 'mobx-react';
+import { observer, renderReporter } from 'mobx-react';
 import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
@@ -1394,7 +1394,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
-        const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc;
+        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? Doc.HighlightBrush.linkFollowEffect, this.rootDoc) : renderDoc;
         return (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 10f2dc016..7235481e0 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -34,6 +34,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
+import { map } from 'bluebird';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -67,17 +68,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * @param presEffectDoc presentation effects document that specifies the animation effect parameters
      * @returns a function that will wrap a JSX animation element wrapping any JSX element
      */
-    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc) {
+    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc, root: Doc) {
         const effectProps = {
-            left: presEffectDoc.presEffectDirection === PresEffect.Left,
-            right: presEffectDoc.presEffectDirection === PresEffect.Right,
-            top: presEffectDoc.presEffectDirection === PresEffect.Top,
-            bottom: presEffectDoc.presEffectDirection === PresEffect.Bottom,
+            left: presEffectDoc?.presEffectDirection === PresEffect.Left,
+            right: presEffectDoc?.presEffectDirection === PresEffect.Right,
+            top: presEffectDoc?.presEffectDirection === PresEffect.Top,
+            bottom: presEffectDoc?.presEffectDirection === PresEffect.Bottom,
             opposite: true,
-            delay: NumCast(presEffectDoc.presTransition),
+            delay: 0,
+            duration: Cast(presEffectDoc?.presTransition, 'number', null),
         };
         //prettier-ignore
-        switch (StrCast(presEffectDoc.presEffect)) {
+        switch (StrCast(presEffectDoc?.presEffect)) {
             default: 
             case PresEffect.None:       return renderDoc;
             case PresEffect.Zoom:       return <Zoom {...effectProps}>{renderDoc}</Zoom>;
@@ -386,7 +388,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         if (pinDataTypes.dataannos) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
-            Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>(DocListCast(activeItem.presAnnotations));
+            Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>([...DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered), ...DocListCast(activeItem.presAnnotations)]);
         }
         if (pinDataTypes.dataview && activeItem.presData !== undefined) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
@@ -465,7 +467,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
             if (pinProps.pinData.dataannos) {
                 const fkey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']));
+                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']).filter(doc => !doc.unrendered));
             }
             if (pinProps.pinData.textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
             if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
@@ -1095,12 +1097,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection.  or better yet, draw everything in an overlay layer instad of within a collection
 
     // Converts seconds to ms and updates presTransition
-    setTransitionTime = (number: String, change?: number) => {
+    public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => {
         let timeInMS = Number(number) * 1000;
         if (change) timeInMS += change;
         if (timeInMS < 100) timeInMS = 100;
         if (timeInMS > 10000) timeInMS = 10000;
-        this.selectedArray.forEach(doc => (doc.presTransition = timeInMS));
+        setter(timeInMS);
+    };
+    setTransitionTime = (number: String, change?: number) => {
+        PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change);
     };
 
     // Converts seconds to ms and updates presTransition
@@ -1165,6 +1170,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     _batch: UndoManager.Batch | undefined = undefined;
 
+    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => {
+        let batch: any;
+        return (
+            <input
+                type="range"
+                step={step}
+                min={min}
+                max={max}
+                value={value}
+                className={`toolbar-slider ${active ? '' : 'none'}`}
+                onPointerDown={e => {
+                    batch = UndoManager.StartBatch('pres slider');
+                    e.stopPropagation();
+                }}
+                onPointerUp={() => batch?.end()}
+                onChange={e => {
+                    e.stopPropagation();
+                    change(e.target.value);
+                }}
+            />
+        );
+    };
     @computed get transitionDropdown() {
         const activeItem: Doc = this.activeItem;
         const targetDoc: Doc = this.targetDoc;
@@ -1180,39 +1207,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 {movement}
             </div>
         );
-        const presDirection = (diretion: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => {
-            const color = this.activeItem.presEffectDirection === diretion || (diretion === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+        const presDirection = (direction: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => {
+            const color = this.activeItem.presEffectDirection === direction || (direction === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
             return (
-                <Tooltip title={<div className="dash-tooltip">{diretion}</div>}>
+                <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                     <div
-                        style={{ ...opts, border: diretion === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
-                        onClick={() => this.updateEffectDirection(diretion)}>
+                        style={{ ...opts, border: direction === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+                        onClick={() => this.updateEffectDirection(direction)}>
                         {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
                     </div>
                 </Tooltip>
             );
         };
-        const inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => {
-            return (
-                <input
-                    type="range"
-                    step={step}
-                    min={min}
-                    max={max}
-                    value={value}
-                    className={`toolbar-slider ${active ? '' : 'none'}`}
-                    onPointerDown={e => {
-                        this._batch = UndoManager.StartBatch('pres slider');
-                        e.stopPropagation();
-                    }}
-                    onPointerUp={() => this._batch?.end()}
-                    onChange={e => {
-                        e.stopPropagation();
-                        change(e.target.value);
-                    }}
-                />
-            );
-        };
         if (activeItem && targetDoc) {
             const type = targetDoc.type;
             const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
@@ -1263,7 +1269,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                             </div>
                         </div>
-                        {inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
+                        {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                             <div className="presBox-subheading">Transition Speed</div>
                             <div className="ribbon-property">
@@ -1278,7 +1284,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                             </div>
                         </div>
-                        {inputter('0.1', '0.1', '10', transitionSpeed, true, this.setTransitionTime)}
+                        {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.setTransitionTime)}
                         <div className={'slider-headers'}>
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
@@ -1331,7 +1337,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         </div>
                                     </div>
                                 </div>
-                                {inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
+                                {PresBox.inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
                                 <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}>
                                     <div className="slider-text">Short</div>
                                     <div className="slider-text">Medium</div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 70cb10970..75801b68c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1263,11 +1263,15 @@ export namespace Doc {
     export function linkFollowUnhighlight() {
         Doc.UnhighlightAll();
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
+        runInAction(() => (HighlightBrush.linkFollowEffect = undefined));
     }
 
     let _lastDate = 0;
-    export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) {
+    export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
         linkFollowUnhighlight();
+        // runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = undefined));
+        // setTimeout(() => runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect)));
+        runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect));
         (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
         document.addEventListener('pointerdown', linkFollowUnhighlight);
@@ -1277,6 +1281,7 @@ export namespace Doc {
 
     export class HighlightBrush {
         @observable HighlightedDoc: Map<Doc, boolean> = new Map();
+        @observable static linkFollowEffect: Doc | undefined;
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 6d32fe60ce32d650a2ba0d5eb8e36dccb591521f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 7 Jan 2023 00:36:06 -0500
Subject: fixed pinning pdf documents without a selection to trail

---
 src/client/views/collections/CollectionSubView.tsx | 2 +-
 src/client/views/collections/CollectionView.tsx    | 1 +
 src/client/views/nodes/trails/PresBox.tsx          | 1 +
 3 files changed, 3 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4f3f21cea..d2074219a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -111,7 +111,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
             }
 
-            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && !d.unrendered).map(d => d as Doc);
+            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc);
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
 
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 917d7618c..a28b1ca19 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -43,6 +43,7 @@ interface CollectionViewProps_ extends FieldViewProps {
     layoutEngine?: () => string;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
     setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => void;
+    ignoreUnrendered?: boolean;
 
     // property overrides for child documents
     childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2e3135416..6f3dc5e82 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -2195,6 +2195,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 PanelHeight={this.panelHeight}
                                 childIgnoreNativeSize={true}
                                 moveDocument={returnFalse}
+                                ignoreUnrendered={true}
                                 //childFitWidth={returnTrue}
                                 childOpacity={returnOne}
                                 childLayoutTemplate={this.childLayoutTemplate}
-- 
cgit v1.2.3-70-g09d2


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

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

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

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


From 5e7989da274606638c96f649e97e9d1a979956f5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Feb 2023 20:57:01 -0500
Subject: cleaning up of following inPlace links to overlay current
 inplaceContainer contents instead of deleting them.

---
 src/client/util/LinkFollower.ts                    |  8 ++---
 src/client/views/DocumentButtonBar.tsx             |  2 +-
 src/client/views/LightboxView.tsx                  | 11 ++----
 src/client/views/PropertiesButtons.tsx             | 39 ++++++++++------------
 src/client/views/PropertiesView.tsx                |  6 ++--
 src/client/views/collections/CollectionSubView.tsx |  1 +
 src/client/views/collections/TabDocView.tsx        | 12 ++++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 ++-
 src/client/views/nodes/DocumentView.tsx            |  9 ++---
 src/client/views/nodes/PDFBox.tsx                  | 10 +++++-
 src/client/views/nodes/WebBox.tsx                  |  9 ++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 +++++
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 13 files changed, 72 insertions(+), 49 deletions(-)

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

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index bbfd516da..50bc89ed2 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,11 +1,11 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { List } from '../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
-import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
 import { PresBox } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
@@ -35,7 +35,7 @@ export class LinkFollower {
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
             const createTabForTarget = (didFocus: boolean) =>
                 new Promise<ViewAdjustment>(res => {
-                    const where = LightboxView.LightboxDoc ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
+                    const where = LightboxView.LightboxDoc ? OpenWhere.lightbox : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
                     docViewProps.addDocTab(doc, where);
                     setTimeout(() => {
                         const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 20c35739e..af868ba9c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -243,7 +243,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                 <div
                     className="documentButtonBar-icon documentButtonBar-follow"
                     style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
-                    onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, undefined, false)))}>
+                    onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false)))}>
                     <div className="documentButtonBar-followTypes">
                         {followBtn(
                             true,
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index e531bf71c..c9b1dcfd8 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -145,13 +145,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         }
     }
     public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
-        if (location !== OpenWhere.lightbox) {
-            const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
-            if (inPlaceView) {
-                inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
-                return true;
-            }
-        }
         SelectionManager.DeselectAll();
         return LightboxView.SetLightboxDoc(
             doc,
@@ -318,7 +311,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} />
                 <div
                     className="lightboxView-navBtn"
-                    title={'toggle fit width'}
+                    title="toggle fit width"
                     onClick={e => {
                         e.stopPropagation();
                         LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
@@ -327,7 +320,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 </div>
                 <div
                     className="lightboxView-tabBtn"
-                    title={'open in tab'}
+                    title="open in tab"
                     onClick={e => {
                         e.stopPropagation();
                         CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a152b4948..7e33ee495 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -6,6 +6,7 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { RichTextField } from '../../fields/RichTextField';
+import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { DocUtils } from '../documents/Documents';
@@ -129,27 +130,21 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
-                    containerDoc.followAllLinks =
-                        containerDoc.noShadow =
-                        containerDoc.noHighlighting =
-                        containerDoc._isLinkButton =
-                        containerDoc._fitContentsToBox =
-                        containerDoc._forceActive =
-                        containerDoc._isInPlaceContainer =
-                            !containerDoc._isInPlaceContainer;
-                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? OpenWhere.inPlace : undefined;
+                    //containerDoc.followAllLinks =
+                    // containerDoc.noShadow =
+                    // containerDoc.noHighlighting =
+                    // containerDoc._forceActive =
+                    containerDoc._fitContentsToBox = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
-                    const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
-                    if (menuDoc) {
-                        menuDoc.hideDecorations = menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer;
-                        if (!dv.allLinks.find(link => link.anchor1 === menuDoc || link.anchor2 === menuDoc)) {
-                            DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
-                        }
-                        DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
-                            menuItem.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true;
-                            menuItem._followLinkLocation = OpenWhere.inPlace;
+                    const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
+                    dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+                    containerContents.forEach(doc => {
+                        DocListCast(doc.links).forEach(link => {
+                            doc.isLinkButton = true;
+                            //doc.followLinkLocation = OpenWhere.inPlace;
+                            link.linkDisplay = false;
                         });
-                    }
+                    });
                 });
             }
         );
@@ -309,10 +304,12 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         docView.setToggleDetail();
                         break;
                     case 'linkInPlace':
-                        docView.toggleFollowLink('inPlace', false, false);
+                        docView.toggleFollowLink(false, false);
+                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.inPlace : undefined;
                         break;
                     case 'linkOnRight':
-                        docView.toggleFollowLink('add:right', false, false);
+                        docView.toggleFollowLink(false, false);
+                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.addRight : undefined;
                         break;
                 }
             });
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f7708d801..e9d1433c3 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1406,7 +1406,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     });
 
     @undoBatch
-    changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
+    changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
@@ -1615,8 +1615,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                     <div className="propertiesView-section">
                                         <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}>
                                             <p>Follow by</p>
-                                            <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkLocation, 'default')}>
-                                                <option value="default">Default</option>
+                                            <select onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}>
+                                                <option value={undefined}>Default</option>
                                                 <option value={OpenWhere.addLeft}>Opening in new left pane</option>
                                                 <option value={OpenWhere.addRight}>Opening in new right pane</option>
                                                 <option value={OpenWhere.replaceLeft}>Replacing left tab</option>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 27ae3041f..c88ae314e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -67,6 +67,7 @@ export function CollectionSubView<X>(moreProps?: X) {
         // to its children which may be templates.
         // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
+            if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey];
             // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
             // setTimeout changes it outside of the @computed section
             !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>()));
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index b75f315ca..01872df84 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -358,7 +358,17 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
         // prettier-ignore
         switch (whereFields[0]) {
-            case OpenWhere.inPlace:  // fall through to lightbox
+            case undefined:
+            case OpenWhere.inPlace:    {
+                if (this.layoutDoc?.isInPlaceContainer) {
+                    const inPlaceView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
+                    const data = inPlaceView?.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)];
+                    if (inPlaceView && (!data || data instanceof List)) {
+                        inPlaceView.layoutDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
+                        return true;
+                    }
+                }
+            }
             case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0477c6a16..78804b070 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1395,6 +1395,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
     addDocTab = action((doc: Doc, where: OpenWhere) => {
+        if (this.props.isAnnotationOverlay) return this.props.addDocTab(doc, where);
         switch (where) {
             case OpenWhere.inParent:
                 return this.props.addDocument?.(doc) || false;
@@ -1412,9 +1413,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         (!docContext || this.props.removeDocument?.(docContext))) ||
                     false
                 );
+            case undefined:
             case OpenWhere.inPlace:
                 if (this.layoutDoc.isInPlaceContainer) {
-                    this.dataDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
+                    this.layoutDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
                     return true;
                 }
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 039a75a1b..beb0f8e3a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -792,7 +792,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    toggleFollowLink = (location: Opt<string>, zoom?: boolean, setTargetToggle?: boolean): void => {
+    toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => {
         this.Document.ignoreClick = false;
         if (setTargetToggle) {
             this.Document.followLinkToggle = !this.Document.followLinkToggle;
@@ -802,7 +802,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         if (this.Document._isLinkButton && !this.onClickHandler) {
             zoom !== undefined && (this.Document.followLinkZoom = zoom);
-            this.Document.followLinkLocation = location;
         } else if (this.Document._isLinkButton && this.onClickHandler) {
             this.Document._isLinkButton = false;
             this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
@@ -985,10 +984,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
 
                     onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' });
-                    onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, false), icon: 'link' });
-                    !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' });
-                    onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' });
-                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
+                    onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
+                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(false, true), icon: 'map-pin' });
                     onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
                 } else if (DocListCast(this.Document.links).length) {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8d7ab2f0d..6b2f2246a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -22,7 +22,7 @@ import { LightboxView } from '../LightboxView';
 import { CreateImage } from '../nodes/WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { SidebarAnnos } from '../SidebarAnnos';
-import { DocFocusOptions, DocumentView } from './DocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
@@ -200,6 +200,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
 
+    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+            this.toggleSidebar(false);
+            return true;
+        }
+        return this.props.addDocTab(doc, where);
+    };
     scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(anchor) && !this.SidebarShown) {
@@ -487,6 +494,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     <PDFViewer
                         {...this.props}
                         rootDoc={this.rootDoc}
+                        addDocTab={this.sidebarAddDocTab}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
                         pdf={this._pdf!}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index caab18e0d..f5eab51c2 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -29,7 +29,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentView, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
@@ -322,6 +322,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return undefined;
     };
 
+    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+            this.toggleSidebar(false);
+            return true;
+        }
+        return this.props.addDocTab(doc, where);
+    };
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         let ele: Opt<HTMLDivElement> = undefined;
         try {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 123499647..dc7a11e6e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -230,6 +230,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
 
+    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+            this.toggleSidebar(false);
+            return true;
+        }
+        return this.props.addDocTab(doc, where);
+    };
+
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         if (!pinProps) return this.rootDoc;
         const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 9877690f8..ffc49e4f3 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -16,7 +16,7 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps, OpenWhere } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { StyleProp } from '../StyleProvider';
-- 
cgit v1.2.3-70-g09d2


From b599afe5fdcf8afc38432e34b1e175f82499d25b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 11 Mar 2023 14:05:18 -0500
Subject: changed links from being a computed field on documents to being
 accessed by a function call on LinkManager

---
 src/client/documents/Documents.ts                  | 101 ++++++---------------
 src/client/util/CaptureManager.tsx                 |  23 +++--
 src/client/util/CurrentUserUtils.ts                |   3 +-
 src/client/util/LinkFollower.ts                    |   3 +-
 src/client/util/LinkManager.ts                     |  17 +++-
 src/client/util/SelectionManager.ts                |   4 +-
 src/client/views/LightboxView.tsx                  |   9 +-
 src/client/views/PropertiesButtons.tsx             |  10 +-
 src/client/views/PropertiesView.tsx                |   8 +-
 src/client/views/SidebarAnnos.tsx                  |   7 +-
 src/client/views/StyleProvider.tsx                 |  11 ++-
 .../collections/CollectionStackedTimeline.tsx      |   7 +-
 src/client/views/collections/CollectionSubView.tsx |   1 -
 src/client/views/collections/TreeView.tsx          |   3 +-
 src/client/views/linking/LinkMenu.tsx              |   1 -
 src/client/views/linking/LinkMenuItem.tsx          |   6 +-
 src/client/views/nodes/AudioBox.tsx                |   3 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   9 +-
 src/client/views/nodes/VideoBox.tsx                |   5 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  18 ++--
 src/client/views/nodes/trails/PresBox.tsx          |   3 +-
 src/client/views/search/SearchBox.tsx              |   3 +-
 src/fields/util.ts                                 |  23 ++---
 24 files changed, 121 insertions(+), 163 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4dc1c3b71..251e432ef 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { action, runInAction } from 'mobx';
 import { basename } from 'path';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, DocListCastAsync, Field, Initializing, Opt, updateCachedAcls } from '../../fields/Doc';
+import { Doc, DocListCast, Field, Initializing, Opt, updateCachedAcls } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { HtmlField } from '../../fields/HtmlField';
 import { InkField, PointData } from '../../fields/InkField';
@@ -403,7 +403,6 @@ export namespace Docs {
                         nativeDimModifiable: true,
                         nativeHeightUnfrozen: true,
                         forceReflow: true,
-                        links: '@links(self)',
                     },
                 },
             ],
@@ -411,35 +410,35 @@ export namespace Docs {
                 DocumentType.SEARCH,
                 {
                     layout: { view: SearchBox, dataField: defaultDataKey },
-                    options: { _width: 400, links: '@links(self)' },
+                    options: { _width: 400 },
                 },
             ],
             [
                 DocumentType.COLOR,
                 {
                     layout: { view: ColorBox, dataField: defaultDataKey },
-                    options: { _nativeWidth: 220, _nativeHeight: 300, links: '@links(self)' },
+                    options: { _nativeWidth: 220, _nativeHeight: 300 },
                 },
             ],
             [
                 DocumentType.IMG,
                 {
                     layout: { view: ImageBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.WEB,
                 {
                     layout: { view: WebBox, dataField: defaultDataKey },
-                    options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: '@links(self)' },
+                    options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
                 },
             ],
             [
                 DocumentType.COL,
                 {
                     layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: '@links(self)' },
+                    options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1 },
                 },
             ],
             [
@@ -453,35 +452,35 @@ export namespace Docs {
                 DocumentType.VID,
                 {
                     layout: { view: VideoBox, dataField: defaultDataKey },
-                    options: { _currentTimecode: 0, links: '@links(self)' },
+                    options: { _currentTimecode: 0 },
                 },
             ],
             [
                 DocumentType.AUDIO,
                 {
                     layout: { view: AudioBox, dataField: defaultDataKey },
-                    options: { _height: 100, backgroundColor: 'lightGray', _fitWidth: true, forceReflow: true, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _height: 100, forceReflow: true, nativeDimModifiable: true },
                 },
             ],
             [
                 DocumentType.REC,
                 {
                     layout: { view: VideoBox, dataField: defaultDataKey },
-                    options: { _height: 100, backgroundColor: 'pink', links: '@links(self)' },
+                    options: { _height: 100, backgroundColor: 'pink' },
                 },
             ],
             [
                 DocumentType.PDF,
                 {
                     layout: { view: PDFBox, dataField: defaultDataKey },
-                    options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: '@links(self)' },
+                    options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
                 },
             ],
             [
                 DocumentType.MAP,
                 {
                     layout: { view: MapBox, dataField: defaultDataKey },
-                    options: { _height: 600, _width: 800, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _height: 600, _width: 800, nativeDimModifiable: true },
                 },
             ],
             [
@@ -502,7 +501,6 @@ export namespace Docs {
                         description: '',
                         showCaption: 'description',
                         backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area
-                        links: '@links(self)',
                         _removeDropProperties: new List(['isLinkButton']),
                     },
                 },
@@ -527,7 +525,7 @@ export namespace Docs {
                 DocumentType.SCRIPTING,
                 {
                     layout: { view: ScriptingBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
@@ -540,56 +538,56 @@ export namespace Docs {
                 DocumentType.LABEL,
                 {
                     layout: { view: LabelBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)', _singleLine: true },
+                    options: { _singleLine: true },
                 },
             ],
             [
                 DocumentType.EQUATION,
                 {
                     layout: { view: EquationBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)', nativeDimModifiable: true, fontSize: '14px', hideResizeHandles: true, hideDecorationTitle: true },
+                    options: { nativeDimModifiable: true, fontSize: '14px', hideResizeHandles: true, hideDecorationTitle: true },
                 },
             ],
             [
                 DocumentType.FUNCPLOT,
                 {
                     layout: { view: FunctionPlotBox, dataField: defaultDataKey },
-                    options: { nativeDimModifiable: true, links: '@links(self)' },
+                    options: { nativeDimModifiable: true },
                 },
             ],
             [
                 DocumentType.BUTTON,
                 {
                     layout: { view: LabelBox, dataField: 'onClick' },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.SLIDER,
                 {
                     layout: { view: SliderBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.PRES,
                 {
                     layout: { view: PresBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: defaultDataKey },
-                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
+                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%' },
                 },
             ],
             [
                 DocumentType.WEBCAM,
                 {
                     layout: { view: RecordingBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
@@ -603,7 +601,7 @@ export namespace Docs {
                 DocumentType.MARKER,
                 {
                     layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { links: '@links(self)', hideLinkButton: true, pointerEvents: 'none' },
+                    options: { hideLinkButton: true, pointerEvents: 'none' },
                 },
             ],
             [
@@ -611,21 +609,21 @@ export namespace Docs {
                 {
                     // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
                     layout: { view: InkingStroke, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.SCREENSHOT,
                 {
                     layout: { view: ScreenshotBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.COMPARISON,
                 {
                     layout: { view: ComparisonBox, dataField: defaultDataKey },
-                    options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'alias', links: '@links(self)' },
+                    options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'alias' },
                 },
             ],
             [
@@ -640,21 +638,21 @@ export namespace Docs {
                 DocumentType.GROUP,
                 {
                     layout: { view: EmptyBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.DATAVIZ,
                 {
                     layout: { view: DataVizBox, dataField: defaultDataKey },
-                    options: { _fitWidth: true, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _fitWidth: true, nativeDimModifiable: true },
                 },
             ],
             [
                 DocumentType.LOADING,
                 {
                     layout: { view: LoadingBox, dataField: '' },
-                    options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true },
                 },
             ],
         ]);
@@ -881,15 +879,7 @@ export namespace Docs {
         }
 
         export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
-            return InstanceFromProto(
-                Prototypes.get(DocumentType.AUDIO),
-                new AudioField(url),
-                { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any },
-                undefined,
-                undefined,
-                undefined,
-                overwriteDoc
-            );
+            return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function RecordingDocument(url: string, options: DocumentOptions = {}) {
@@ -992,7 +982,6 @@ export namespace Docs {
             I.creationDate = new DateField();
             I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             //I['acl-Override'] = SharingPermissions.Unset;
-            I.links = ComputedField.MakeFunction('links(self)');
             I[Initializing] = false;
             return I;
         }
@@ -1282,38 +1271,6 @@ export namespace DocUtils {
         return rangeFilteredDocs;
     }
 
-    export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
-        targetID = targetID.replace(/^-/, '').replace(/\([0-9]*\)$/, '');
-        DocServer.GetRefField(targetID).then(doc => {
-            if (promoteDoc !== doc) {
-                let copy = doc as Doc;
-                if (copy) {
-                    Doc.Overwrite(promoteDoc, copy, true);
-                } else {
-                    copy = Doc.MakeCopy(promoteDoc, true, targetID);
-                }
-                !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID);
-                addDoc && addDoc(copy);
-                remDoc && remDoc(promoteDoc);
-                if (!doc) {
-                    DocListCastAsync(promoteDoc.links).then(links => {
-                        links &&
-                            links.map(async link => {
-                                if (link) {
-                                    const a1 = await Cast(link.anchor1, Doc);
-                                    if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy;
-                                    const a2 = await Cast(link.anchor2, Doc);
-                                    if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy;
-                                    LinkManager.Instance.deleteLink(link);
-                                    LinkManager.Instance.addLink(link);
-                                }
-                            });
-                    });
-                }
-            }
-        });
-    }
-
     export function DefaultFocus(doc: Doc, options: DocFocusOptions) {
         return undefined;
     }
@@ -1711,7 +1668,7 @@ export namespace DocUtils {
     export function LeavePushpin(doc: Doc, annotationField: string) {
         if (doc.followLinkToggle) return undefined;
         const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
-        const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
+        const hasContextAnchor = LinkManager.Links(doc).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
         if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
             const pushpin = Docs.Create.FontIconDocument({
                 title: 'pushpin',
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 735b06f6d..c9fcc84a3 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -2,12 +2,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast } from '../../fields/Doc';
-import { StrCast } from '../../fields/Types';
+import { Doc } from '../../fields/Doc';
+import { DocCast, StrCast } from '../../fields/Types';
 import { addStyleSheet } from '../../Utils';
 import { LightboxView } from '../views/LightboxView';
 import { MainViewModal } from '../views/MainViewModal';
 import './CaptureManager.scss';
+import { LinkManager } from './LinkManager';
 import { SelectionManager } from './SelectionManager';
 
 @observer
@@ -48,16 +49,14 @@ export class CaptureManager extends React.Component<{}> {
         const doc = this._document;
         const order: JSX.Element[] = [];
         if (doc) {
-            DocListCast(doc.links).forEach((l, i) => {
-                if (l) {
-                    order.push(
-                        <div className="list-item">
-                            <div className="number">{i}</div>
-                            {StrCast((l.anchor1 as Doc).title)}
-                        </div>
-                    );
-                }
-            });
+            LinkManager.Links(doc).forEach((l, i) =>
+                order.push(
+                    <div className="list-item">
+                        <div className="number">{i}</div>
+                        {StrCast(DocCast(l.anchor1)?.title)}
+                    </div>
+                )
+            );
         }
 
         return (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2b0a2cdac..c4fb4788c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -22,7 +22,7 @@ import { Colors } from "../views/global/globalEnums";
 import { MainView } from "../views/MainView";
 import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
 import { OverlayView } from "../views/OverlayView";
-import { DragManager, dropActionType } from "./DragManager";
+import { DragManager } from "./DragManager";
 import { MakeTemplate } from "./DropConverter";
 import { LinkManager } from "./LinkManager";
 import { ScriptingGlobals } from "./ScriptingGlobals";
@@ -949,6 +949,5 @@ ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Do
 ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
 ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "creates a new presentation when called");
 ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called");
-ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)");
 ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
 ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
\ No newline at end of file
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index e28687561..eacbcc0e3 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -6,6 +6,7 @@ import { DocumentDecorations } from '../views/DocumentDecorations';
 import { DocFocusOptions, DocumentViewSharedProps, OpenWhere } from '../views/nodes/DocumentView';
 import { PresBox } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
+import { LinkManager } from './LinkManager';
 import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
 
@@ -41,7 +42,7 @@ export class LinkFollower {
     };
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
-        const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
+        const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
         const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
         const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
         const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor2 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0);
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 46d44fea4..555215417 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -4,6 +4,7 @@ import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '
 import { List } from '../../fields/List';
 import { ProxyField } from '../../fields/Proxy';
 import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { ScriptingGlobals } from './ScriptingGlobals';
 /*
  * link doc:
  * - anchor1: doc
@@ -24,6 +25,10 @@ export class LinkManager {
     public static get Instance() {
         return LinkManager._instance;
     }
+
+    public static Links(doc: Doc | undefined) {
+        return doc ? LinkManager.Instance.getAllRelatedLinks(doc) : [];
+    }
     public static addLinkDB = async (linkDb: any) => {
         await Promise.all(
             ((await DocListCastAsync(linkDb.data)) ?? []).map(link =>
@@ -34,7 +39,7 @@ export class LinkManager {
         LinkManager.userLinkDBs.push(linkDb);
     };
     public static AutoKeywords = 'keywords:Usages';
-    static links: Doc[] = [];
+    static _links: Doc[] = [];
     constructor() {
         LinkManager._instance = this;
         this.createLinkrelationshipLists();
@@ -72,7 +77,7 @@ export class LinkManager {
             );
         };
         const watchUserLinkDB = (userLinkDBDoc: Doc) => {
-            LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
+            LinkManager._links.push(...DocListCast(userLinkDBDoc.data));
             const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts.  data structure is not a simple list of Docs, but a list of ProxyField/Fields
             if (userLinkDBDoc.data) {
                 observe(
@@ -193,3 +198,11 @@ export class LinkManager {
         if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
     }
 }
+
+ScriptingGlobals.add(
+    function links(doc: any) {
+        return new List(LinkManager.Links(doc));
+    },
+    'returns all the links to the document or its annotations',
+    '(doc: any)'
+);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 646942569..c0fc25376 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,6 +1,6 @@
 import { action, observable, ObservableMap } from 'mobx';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { DocCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocumentView } from '../views/nodes/DocumentView';
@@ -22,7 +22,7 @@ export namespace SelectionManager {
             // if doc is not in SelectedDocuments, add it
             if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
                 if (!ctrlPressed) {
-                    if (LinkManager.currentLink && !DocListCast(docView.rootDoc.links).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
+                    if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
                         LinkManager.currentLink = undefined;
                     }
                     this.DeselectAll();
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 2567d44bb..976c8763e 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -4,9 +4,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { InkTool } from '../../fields/InkField';
-import { List } from '../../fields/List';
-import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
+import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
 import { LinkManager } from '../util/LinkManager';
@@ -88,7 +87,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 ...future
                     .slice()
                     .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))
-                    .sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length),
+                    .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length),
             ];
         }
         this._doc = doc;
@@ -214,7 +213,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (coll) {
             const fieldKey = Doc.LayoutFieldKey(coll);
             const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])];
-            const links = DocListCast(coll.links)
+            const links = LinkManager.Links(coll)
                 .map(link => LinkManager.getOppositeAnchor(link, coll))
                 .filter(doc => doc)
                 .map(doc => doc!);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ac1b66013..ebbe20077 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -9,8 +9,10 @@ import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
+import { Utils } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
@@ -19,8 +21,6 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
 import React = require('react');
-import { LinkManager } from '../util/LinkManager';
-import { Utils } from '../../Utils';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -139,9 +139,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
                     dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
-                    containerContents.forEach(doc => {
-                        DocListCast(doc.links).forEach(link => (link.linkDisplay = false));
-                    });
+                    containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.linkDisplay = false)));
                 });
             }
         );
@@ -330,7 +328,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             const click = () => this.handleOptionChange(value[0]);
             const linkButton = BoolCast(this.selectedDoc._isLinkButton);
             const followLoc = this.selectedDoc._followLinkLocation;
-            const linkedToLightboxView = () => DocListCast(this.selectedDoc.links).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+            const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
 
             let active = false;
             // prettier-ignore
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 7a985628f..03b4100a7 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -4,10 +4,10 @@ import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-soli
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox, Tooltip } from '@material-ui/core';
 import { intersection } from 'lodash';
-import { action, autorun, computed, Lambda, observable } from 'mobx';
+import { action, computed, Lambda, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
-import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
+import { AclAdmin, AclSym, DataSym, Doc, Field, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
@@ -23,10 +23,10 @@ import { SharingManager } from '../util/SharingManager';
 import { Transform } from '../util/Transform';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { EditableView } from './EditableView';
+import { FilterPanel } from './FilterPanel';
 import { Colors } from './global/globalEnums';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
-import { FilterPanel } from './FilterPanel';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
 import { PropertiesButtons } from './PropertiesButtons';
@@ -1486,7 +1486,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
         const indent = 30;
-        const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!));
+        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
         if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this.props.width }}>
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 74ea624a6..7519cbb05 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,10 +3,12 @@ import { observer } from 'mobx-react';
 import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
+import { RichTextField } from '../../fields/RichTextField';
 import { DocCast, NumCast, StrCast } from '../../fields/Types';
 import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
 import { Transform } from '../util/Transform';
 import { CollectionStackingView } from './collections/CollectionStackingView';
 import { FieldViewProps } from './nodes/FieldView';
@@ -15,7 +17,6 @@ import { SearchBox } from './search/SearchBox';
 import './SidebarAnnos.scss';
 import { StyleProp } from './StyleProvider';
 import React = require('react');
-import { RichTextField } from '../../fields/RichTextField';
 
 interface ExtraProps {
     fieldKey: string;
@@ -174,8 +175,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     showTitle = () => 'title';
     setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight());
     sortByLinkAnchorY = (a: Doc, b: Doc) => {
-        const ay = DocListCast(a.links).length && DocCast(DocListCast(a.links)[0].anchor1).y;
-        const by = DocListCast(b.links).length && DocCast(DocListCast(b.links)[0].anchor1).y;
+        const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].anchor1).y;
+        const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].anchor1).y;
         return NumCast(ay) - NumCast(by);
     };
     render() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 153c30052..ce764c7bf 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,12 +1,15 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Shadows } from 'browndash-components';
 import { action, runInAction } from 'mobx';
 import { extname } from 'path';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { Doc, Opt } from '../../fields/Doc';
+import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types';
 import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen } from '../util/DocumentManager';
+import { LinkManager } from '../util/LinkManager';
+import { SelectionManager } from '../util/SelectionManager';
 import { ColorScheme } from '../util/SettingsManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { TreeSort } from './collections/TreeView';
@@ -18,8 +21,6 @@ import { FieldViewProps } from './nodes/FieldView';
 import { SliderBox } from './nodes/SliderBox';
 import './StyleProvider.scss';
 import React = require('react');
-import { Shadows } from 'browndash-components';
-import { SelectionManager } from '../util/SelectionManager';
 
 export enum StyleProp {
     TreeViewIcon = 'treeViewIcon',
@@ -261,7 +262,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.BoxShadow: {
             if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
             if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
-            if (doc?.isLinkButton && DocListCast(doc?.links).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
+            if (doc?.isLinkButton && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
             switch (doc?.type) {
                 case DocumentType.COL:
                     return StrCast(
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 572fbfec2..5bdff347c 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,7 +2,7 @@ import React = require('react');
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -15,6 +15,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
+import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -740,14 +741,14 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
             () => this.props.currentTimecode(),
             time => {
                 const dictationDoc = Cast(this.props.layoutDoc['data-dictation'], Doc, null);
-                const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
+                const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
                 if (
                     !LightboxView.LightboxDoc &&
                     // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
                     // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
                     /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
                     !this.props.layoutDoc.dontAutoFollowLinks &&
-                    DocListCast(this.props.mark.links).length &&
+                    LinkManager.Links(this.props.mark).length &&
                     time > NumCast(this.props.mark[this.props.startTag]) &&
                     time < NumCast(this.props.mark[this.props.endTag]) &&
                     this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index c88ae314e..e46220f02 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,5 +1,4 @@
 import { action, computed, observable } from 'mobx';
-import ReactLoading from 'react-loading';
 import * as rp from 'request-promise';
 import CursorField from '../../../fields/CursorField';
 import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 08ffa1466..2bdcf472f 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -15,6 +15,7 @@ import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -708,7 +709,7 @@ export class TreeView extends React.Component<TreeViewProps> {
 
     @computed get validExpandViewTypes() {
         const annos = () => (DocListCast(this.doc[this.fieldKey + '-annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : '');
-        const links = () => (DocListCast(this.doc.links).length && !this.props.treeView.dashboardMode ? 'links' : '');
+        const links = () => (LinkManager.Links(this.doc).length && !this.props.treeView.dashboardMode ? 'links' : '');
         const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : '');
         const aliases = () => (this.props.treeView.dashboardMode ? '' : 'aliases');
         const fields = () => (Doc.noviceMode ? '' : 'fields');
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index c9112eec3..3f6369898 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,7 +1,6 @@
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../../fields/Doc';
-import { DocCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index cdac91a62..4741fc6f2 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
 import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
@@ -15,11 +15,11 @@ import { LinkManager } from '../../util/LinkManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { undoBatch } from '../../util/UndoManager';
+import { MainView } from '../MainView';
 import { DocumentView, OpenWhere } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
 import React = require('react');
-import { MainView } from '../MainView';
 
 interface LinkMenuItemProps {
     groupType: string;
@@ -34,7 +34,7 @@ interface LinkMenuItemProps {
 
 // drag links and drop link targets (aliasing them if needed)
 export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
-    const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+    const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
 
     if (draggedDocs.length) {
         const moddrag: Doc[] = [];
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 4c26468b9..890ecc1b2 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -11,6 +11,7 @@ import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } f
 import { DocUtils } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
@@ -84,7 +85,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return this.props.PanelHeight() < 50;
     } // used to collapse timeline when node is shrunk
     @computed get links() {
-        return DocListCast(this.dataDoc.links);
+        return LinkManager.Links(this.dataDoc);
     }
     @computed get mediaState() {
         return this.dataDoc.mediaState as media_state;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2b6629913..964231e9d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -929,7 +929,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     },
                     icon: 'eye',
                 });
-            DocListCast(this.Document.links).length &&
+            LinkManager.Links(this.Document).length &&
                 appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
             !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
@@ -969,7 +969,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(false, true), icon: 'map-pin' });
                     onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
-                } else if (DocListCast(this.Document.links).length) {
+                } else if (LinkManager.Links(this.Document).length) {
                     onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' });
                     onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' });
                     onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' });
@@ -1907,7 +1907,7 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) {
     const linkSource = Cast(linkCollection.linkSource, Doc, null);
     const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
     let wid = linkSource[WidthSym]();
-    const links = DocListCast(linkSource.links);
+    const links = LinkManager.Links(linkSource);
     links.forEach(link => {
         const other = LinkManager.getOppositeAnchor(link, linkSource);
         const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index bbe9b4323..16b352e4f 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import wiki from 'wikijs';
-import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
 import { DocServer } from '../../DocServer';
@@ -14,11 +14,10 @@ import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
+import { SearchBox } from '../search/SearchBox';
 import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
-import { SearchUtil } from '../../util/SearchUtil';
-import { SearchBox } from '../search/SearchBox';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
@@ -110,8 +109,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                 const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined;
                 anchorDoc?.then?.(
                     action(anchor => {
-                        if (anchor instanceof Doc && DocListCast(anchor.links).length) {
-                            this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0];
+                        if (anchor instanceof Doc && LinkManager.Links(anchor).length) {
+                            this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0];
                             const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords;
                             if (automaticLink) {
                                 // automatic links specify the target in the link info, not the source
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 7df3d5f24..dc6bf6f9e 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
 import { observer } from 'mobx-react';
 import { basename } from 'path';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
@@ -14,6 +14,7 @@ import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
 import { DocumentManager } from '../../util/DocumentManager';
+import { LinkManager } from '../../util/LinkManager';
 import { ReplayMovements } from '../../util/ReplayMovements';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -85,7 +86,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @observable _scrubbing: boolean = false;
 
     @computed get links() {
-        return DocListCast(this.dataDoc.links);
+        return LinkManager.Links(this.dataDoc);
     }
     @computed get heightPercent() {
         return NumCast(this.layoutDoc._timelineHeightPercent, 100);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d9ce9a0b0..a76f9623a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { isEqual } from 'lodash';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { baseKeymap, selectAll } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
@@ -11,7 +11,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
 import { EditorView } from 'prosemirror-view';
 import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -48,6 +48,7 @@ import { StyleProp } from '../../StyleProvider';
 import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
+import { PinProps, PresBox } from '../trails';
 import { DashDocCommentView } from './DashDocCommentView';
 import { DashDocView } from './DashDocView';
 import { DashFieldView } from './DashFieldView';
@@ -63,7 +64,6 @@ import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
-import { PinProps, PresBox } from '../trails';
 const translateGoogleApi = require('translate-google-api');
 
 export const GoogleRef = 'googleDocId';
@@ -363,7 +363,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         let linkTime;
         let linkAnchor;
         let link;
-        DocListCast(this.dataDoc.links).forEach((l, i) => {
+        LinkManager.Links(this.dataDoc).forEach((l, i) => {
             const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined;
             if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') {
                 linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
@@ -400,7 +400,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
     autoLink = () => {
         const newAutoLinks = new Set<Doc>();
-        const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+        const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
         if (this._editorView?.state.doc.textContent) {
             const isNodeSel = this._editorView.state.selection instanceof NodeSelection;
             const f = this._editorView.state.selection.from;
@@ -453,7 +453,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                             alink =
                                 alink ??
-                                (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
+                                (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
                                     DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
                             newAutoLinks.add(alink);
                             const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
@@ -687,7 +687,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
     @undoBatch
     deleteAnnotation = (anchor: Doc) => {
-        LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]);
+        LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
         // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]);
         // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
         // AnchorMenu.Instance.fadeOut(true);
@@ -1014,7 +1014,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     }
     componentDidMount() {
         !this.props.dontSelectOnLoad && 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._cachedLinks = DocListCast(this.Document.links);
+        this._cachedLinks = LinkManager.Links(this.Document);
         this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation);
         this._disposers.autoHeight = reaction(
             () => this.autoHeight,
@@ -1041,7 +1041,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             { fireImmediately: true }
         );
         this._disposers.links = reaction(
-            () => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+            () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
             newLinks => {
                 this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
                 this._cachedLinks = newLinks;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index dda96d8e7..be40b3592 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -35,7 +35,6 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { tag } from 'xregexp/types';
 const { Howl } = require('howler');
 
 export interface pinDataTypes {
@@ -1551,7 +1550,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
                                 activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0;
                                 // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
-                                // to avoid creating a new slide to correspond to each of the target's data list, we simply reference the target's data list.
+                                // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
                                 let dataField = Doc.LayoutFieldKey(tagDoc);
                                 if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '-annotations';
 
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 3ba60edd7..fe9b13fbe 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -5,7 +5,6 @@ import * as React from 'react';
 import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, StrCast } from '../../../fields/Types';
-import { StopEvent } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -470,7 +469,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                                       }
                             }
                             style={{
-                                fontWeight: DocListCast(fromDoc?.links).find(
+                                fontWeight: LinkManager.Links(fromDoc).find(
                                     link => Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, fromDoc!), result[0] as Doc) || Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, fromDoc!)?.annotationOn), result[0] as Doc)
                                 )
                                     ? 'bold'
diff --git a/src/fields/util.ts b/src/fields/util.ts
index e517e7604..6ff13d5d3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,8 +1,8 @@
-import { forEach } from 'lodash';
 import { $mobx, action, observable, runInAction, trace } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { DocServer } from '../client/DocServer';
 import { CollectionViewType } from '../client/documents/DocumentTypes';
+import { LinkManager } from '../client/util/LinkManager';
 import { SerializationHelper } from '../client/util/SerializationHelper';
 import { UndoManager } from '../client/util/UndoManager';
 import { returnZero } from '../Utils';
@@ -18,7 +18,6 @@ import {
     Doc,
     DocListCast,
     DocListCastAsync,
-    FieldResult,
     ForceServerWrite,
     HeightSym,
     HierarchyMapping,
@@ -259,7 +258,7 @@ function getEffectiveAcl(target: any, user?: string): symbol {
  */
 export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
     if (!visited) visited = [] as Doc[];
-    if (visited.includes(target)) return;
+    if (!target || visited.includes(target)) return;
 
     if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
         target[key] = acl;
@@ -293,26 +292,18 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
         }
 
         // maps over the links of the document
-        DocListCast(dataDoc.links).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+        LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
 
         // maps over the children of the document
-        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => {
+        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
             distributeAcls(key, acl, d, inheritingFromCollection, visited);
-            // }
-            const data = d[DataSym];
-            if (data) {
-                distributeAcls(key, acl, data, inheritingFromCollection, visited);
-            }
+            distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited);
         });
 
         // maps over the annotations of the document
-        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => {
+        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).forEach(d => {
             distributeAcls(key, acl, d, inheritingFromCollection, visited);
-            // }
-            const data = d[DataSym];
-            if (data) {
-                distributeAcls(key, acl, data, inheritingFromCollection, visited);
-            }
+            distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited);
         });
     }
 
-- 
cgit v1.2.3-70-g09d2


From b25f333713706a7456ab418960082bcabe830f11 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 20 Mar 2023 20:48:38 -0400
Subject: fixed copying of docs with template layout docs.  fixed clone to
 search through RTFs for referenced documents to clone.  renamed nested
 documents docId instead of docid for consistency with other nested field ids.

---
 src/client/util/CurrentUserUtils.ts                |  6 +--
 src/client/views/GlobalKeyHandler.ts               |  2 +-
 src/client/views/SidebarAnnos.tsx                  |  4 +-
 src/client/views/collections/CollectionSubView.tsx | 10 ++---
 src/client/views/nodes/DocumentView.tsx            |  6 +--
 .../nodes/formattedText/DashDocCommentView.tsx     | 16 ++++----
 .../views/nodes/formattedText/DashDocView.tsx      | 16 ++++----
 .../views/nodes/formattedText/DashFieldView.tsx    |  8 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 ++--
 .../views/nodes/formattedText/RichTextRules.ts     | 26 ++++++------
 src/client/views/nodes/formattedText/nodes_rts.ts  |  8 ++--
 src/fields/Doc.ts                                  | 46 ++++++++++++++--------
 src/fields/RichTextField.ts                        | 26 ++++++------
 src/fields/RichTextUtils.ts                        | 10 ++---
 14 files changed, 103 insertions(+), 89 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2820c66ee..c038fd6ab 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -148,7 +148,7 @@ export class CurrentUserUtils {
             { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
             { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
         const reqdNoteList = reqdTempOpts.map(opts => {
-            const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true, system: true};
+            const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true};
             const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
             return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
         });
@@ -224,11 +224,11 @@ export class CurrentUserUtils {
                     {
                         type: "paragraph", attrs: {}, content: [{
                             type: "dashField",
-                            attrs: { fieldKey: "author", docid: "", hideKey: false },
+                            attrs: { fieldKey: "author", docId: "", hideKey: false },
                             marks: [{ type: "strong" }]
                         }, {
                             type: "dashField",
-                            attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
+                            attrs: { fieldKey: "creationDate", docId: "", hideKey: false },
                             marks: [{ type: "strong" }]
                         }]
                     }]
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f849b21e3..0f8b46dbe 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -372,7 +372,7 @@ export class KeyManager {
                                 list.push(doc);
                             }
                             if (count === docids.length) {
-                                const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, ['links'])).clone : d)));
+                                const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d)));
                                 if (added.length) {
                                     added.map(doc => (doc.context = targetDataDoc));
                                     undoBatch(() => {
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 7519cbb05..02dd15960 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -84,7 +84,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 Doc.GetProto(target)[key] = val;
                 return {
                     type: 'dashField',
-                    attrs: { fieldKey: key, docid: '', hideKey: false, editable: true },
+                    attrs: { fieldKey: key, docId: '', hideKey: false, editable: true },
                     marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }],
                 };
             });
@@ -111,7 +111,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                             { type: 'pFontSize', attrs: { fontSize: '8px' } },
                             { type: 'em' },
                         ],
-                        attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false },
+                        attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, editable: false },
                     },
                 ],
             },
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e46220f02..bd74c9399 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -269,10 +269,10 @@ export function CollectionSubView<X>(moreProps?: X) {
                 if (FormattedTextBox.IsFragment(html)) {
                     const href = FormattedTextBox.GetHref(html);
                     if (href) {
-                        const docid = FormattedTextBox.GetDocFromUrl(href);
-                        if (docid) {
+                        const docId = FormattedTextBox.GetDocFromUrl(href);
+                        if (docId) {
                             // prosemirror text containing link to dash document
-                            DocServer.GetRefField(docid).then(f => {
+                            DocServer.GetRefField(docId).then(f => {
                                 if (f instanceof Doc) {
                                     if (options.x || options.y) {
                                         f.x = options.x as number;
@@ -311,8 +311,8 @@ export function CollectionSubView<X>(moreProps?: X) {
                     } else {
                         const path = window.location.origin + '/doc/';
                         if (text.startsWith(path)) {
-                            const docid = text.replace(Doc.globalServerPath(), '').split('?')[0];
-                            DocServer.GetRefField(docid).then(f => {
+                            const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
+                            DocServer.GetRefField(docId).then(f => {
                                 if (f instanceof Doc) {
                                     if (options.x || options.y) {
                                         f.x = options.x as number;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 36e8facf5..edf508c95 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1578,8 +1578,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     }
 
     public static showBackLinks(linkSource: Doc) {
-        const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
-        DocServer.GetRefField(docid).then(docx => {
+        const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
+        DocServer.GetRefField(docId).then(docx => {
             const rootAlias = () => {
                 const rootAlias = Doc.MakeAlias(linkSource);
                 rootAlias.x = rootAlias.y = 0;
@@ -1592,7 +1592,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         /*rootAlias()*/
                     ],
                     { title: linkSource.title + '-pivot', _width: 500, _height: 500 },
-                    docid
+                    docId
                 );
             linkCollection.linkSource = linkSource;
             if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)');
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index fcd6e0c55..aa269d8d6 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -32,7 +32,7 @@ export class DashDocCommentView {
         };
 
         this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
+        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
         (this as any).dom = this.dom;
     }
 
@@ -48,7 +48,7 @@ export class DashDocCommentView {
 }
 
 interface IDashDocCommentViewInternal {
-    docid: string;
+    docId: string;
     view: any;
     getPos: any;
 }
@@ -63,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
     }
 
     onPointerLeaveCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
         e.preventDefault();
         e.stopPropagation();
     }
 
     onPointerEnterCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
         e.preventDefault();
         e.stopPropagation();
     }
@@ -82,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
             const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
             this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
             setTimeout(() => {
-                expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+                expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
                 try {
                     this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
                 } catch (e) {}
@@ -100,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         const state = this.props.view.state;
         for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) {
             const m = state.doc.nodeAt(i);
-            if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) {
+            if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) {
                 return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean };
             }
         }
 
-        const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' });
+        const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' });
         this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc));
         setTimeout(() => {
             try {
@@ -119,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         return (
             <span
                 className="formattedTextBox-inlineComment"
-                id={'DashDocCommentView-' + this.props.docid}
+                id={'DashDocCommentView-' + this.props.docId}
                 onPointerLeave={this.onPointerLeaveCollapsed}
                 onPointerEnter={this.onPointerEnterCollapsed}
                 onPointerUp={this.onPointerUpCollapsed}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 19ee1fc7b..61345f891 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -42,7 +42,7 @@ export class DashDocView {
 
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
-            <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
+            <DashDocViewInternal docId={node.attrs.docId} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
         );
     }
     destroy() {
@@ -53,7 +53,7 @@ export class DashDocView {
 }
 
 interface IDashDocViewInternal {
-    docid: string;
+    docId: string;
     alias: string;
     tbox: FormattedTextBox;
     width: string;
@@ -77,7 +77,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
 
     updateDoc = action((dashDoc: Doc) => {
         this._dashDoc = dashDoc;
-        this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
+        this._finalLayout = this.props.docId ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
 
         if (this._finalLayout) {
             if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
@@ -107,12 +107,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
         super(props);
         this._textBox = this.props.tbox;
 
-        DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
+        DocServer.GetRefField(this.props.docId + this.props.alias).then(async dashDoc => {
             if (!(dashDoc instanceof Doc)) {
                 this.props.alias &&
-                    DocServer.GetRefField(this.props.docid).then(async dashDocBase => {
+                    DocServer.GetRefField(this.props.docId).then(async dashDocBase => {
                         if (dashDocBase instanceof Doc) {
-                            const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias);
+                            const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docId + this.props.alias);
                             aliasedDoc.layoutKey = 'layout';
                             this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
                             this.updateDoc(aliasedDoc);
@@ -161,12 +161,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
     };
 
     onPointerLeave = () => {
-        const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+        const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
         ele && (ele.style.backgroundColor = '');
     };
 
     onPointerEnter = () => {
-        const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+        const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
         ele && (ele.style.backgroundColor = 'orange');
     };
 
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index a89e8b4ed..21ccf3bc7 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -54,7 +54,7 @@ export class DashFieldView {
                 unclickable={this.unclickable}
                 getPos={getPos}
                 fieldKey={node.attrs.fieldKey}
-                docid={node.attrs.docid}
+                docId={node.attrs.docId}
                 width={node.attrs.width}
                 height={node.attrs.height}
                 hideKey={node.attrs.hideKey}
@@ -76,7 +76,7 @@ export class DashFieldView {
 
 interface IDashFieldViewInternal {
     fieldKey: string;
-    docid: string;
+    docId: string;
     hideKey: boolean;
     tbox: FormattedTextBox;
     width: number;
@@ -100,8 +100,8 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
         this._fieldKey = this.props.fieldKey;
         this._textBoxDoc = this.props.tbox.props.Document;
 
-        if (this.props.docid) {
-            DocServer.GetRefField(this.props.docid).then(
+        if (this.props.docId) {
+            DocServer.GetRefField(this.props.docId).then(
                 action(async dashDoc => {
                     dashDoc instanceof Doc && (this._dashDoc = dashDoc);
                 })
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a76f9623a..da10bb0dc 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -186,7 +186,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return '';
     }
     public static GetDocFromUrl(url: string) {
-        return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid
+        return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
     }
 
     constructor(props: any) {
@@ -545,7 +545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     width: target[WidthSym](),
                     height: target[HeightSym](),
                     title: 'dashDoc',
-                    docid: target[Id],
+                    docId: target[Id],
                     float: 'unset',
                 });
                 if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) {
@@ -1260,7 +1260,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
                 if (pdfAnchor instanceof Doc) {
                     const dashField = view.state.schema.nodes.paragraph.create({}, [
-                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
                             view.state.schema.marks.linkAnchor.create({
                                 allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }],
                                 location: 'add:right',
@@ -1485,7 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     onPointerUp = (e: React.PointerEvent): void => {
         const editor = this._editorView!;
         const state = editor?.state;
-        if (!state || !editor) return;
+        if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
         if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
         else if (this.props.isContentActive(true)) {
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 5675776fb..9e9b61db3 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -89,8 +89,8 @@ export class RichTextRules {
                 textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
                 textDoc[inlineFieldKey] = ''; // set a default value for the annotation
                 const node = (state.doc.resolve(start) as any).nodeAfter;
-                const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
-                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' });
+                const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
                 const sm = state.storedMarks || undefined;
                 const replaced = node
                     ? state.tr
@@ -248,17 +248,17 @@ export class RichTextRules {
             // [[fieldKey:Doc]] => show field of doc
             new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
                 const fieldKey = match[1];
-                const docid = match[3]?.replace(':', '');
+                const docId = match[3]?.replace(':', '');
                 const value = match[2]?.substring(1);
                 if (!fieldKey) {
-                    if (docid) {
-                        DocServer.GetRefField(docid).then(docx => {
+                    if (docId) {
+                        DocServer.GetRefField(docId).then(docx => {
                             const rstate = this.TextBox.EditorView?.state;
                             const selection = rstate?.selection.$from.pos;
                             if (rstate) {
                                 this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
                             }
-                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
+                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId);
                             DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
 
                             const fstate = this.TextBox.EditorView?.state;
@@ -274,7 +274,7 @@ export class RichTextRules {
                     const num = value.match(/^[0-9.]$/);
                     this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
                 }
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false });
+                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false });
                 return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
             }),
 
@@ -311,16 +311,16 @@ export class RichTextRules {
                 const fieldKey = match[1] || '';
                 const fieldParam = match[2]?.replace('…', '...') || '';
                 const rawdocid = match[3]?.substring(1);
-                const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
-                if (!fieldKey && !docid) return state.tr;
-                docid &&
-                    DocServer.GetRefField(docid).then(docx => {
+                const docId = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
+                if (!fieldKey && !docId) return state.tr;
+                docId &&
+                    DocServer.GetRefField(docId).then(docx => {
                         if (!(docx instanceof Doc && docx)) {
-                            Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid);
+                            Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docId);
                         }
                     });
                 const node = (state.doc.resolve(start) as any).nodeAfter;
-                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
+                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docId, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
                 const sm = state.storedMarks || undefined;
                 return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
             }),
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index aa2475dca..6c9d5d31a 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = {
 
     dashComment: {
         attrs: {
-            docid: { default: '' },
+            docId: { default: '' },
         },
         inline: true,
         group: 'inline',
@@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             title: { default: null },
             float: { default: 'left' },
             location: { default: 'add:right' },
-            docid: { default: '' },
+            docId: { default: '' },
         },
         group: 'inline',
         draggable: true,
@@ -246,7 +246,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             float: { default: 'right' },
             hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
             fieldKey: { default: '' },
-            docid: { default: '' },
+            docId: { default: '' },
             alias: { default: '' },
         },
         group: 'inline',
@@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = {
         inline: true,
         attrs: {
             fieldKey: { default: '' },
-            docid: { default: '' },
+            docId: { default: '' },
             hideKey: { default: false },
             editable: { default: true },
         },
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 40ef67f92..40da482d2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -695,6 +695,9 @@ export namespace Doc {
         return bestAlias ?? Doc.MakeAlias(doc);
     }
 
+    // this lists out all the tag ids that can be  in a RichTextField that might contain document ids.
+    // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
+    export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
     export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean, asBranch: boolean): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
@@ -713,12 +716,22 @@ export namespace Doc {
                     if (docs !== undefined && docs.length) {
                         const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch)));
                         assignKey(new List<Doc>(clones));
-                    } else if (doc[key] instanceof Doc) {
-                        assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch)); // reference documents except copy documents that are expanded template fields
                     } else {
                         assignKey(ObjectField.MakeCopy(field));
                         if (field instanceof RichTextField) {
-                            if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
+                            if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) {
+                                const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g');
+                                const rawdocids = field.Data.match(docidsearch);
+                                const docids = rawdocids?.map((str: string) =>
+                                    DocsInTextFieldIds.reduce((output, exp) => {
+                                        return output.replace(new RegExp(`${exp}":`, 'g'), '');
+                                    }, str)
+                                        .replace(/"/g, '')
+                                        .trim()
+                                );
+                                const results = docids && (await DocServer.GetRefFields(docids));
+                                const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
+                                docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
                                 rtfs.push({ copy, key, field });
                             }
                         }
@@ -726,24 +739,22 @@ export namespace Doc {
                 };
                 const docAtKey = doc[key];
                 if (docAtKey instanceof Doc) {
-                    if (!Doc.IsSystem(docAtKey) && (key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
+                    if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
                         assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
                     } else {
                         assignKey(docAtKey);
                     }
+                } else if (field instanceof RefField) {
+                    assignKey(field);
+                } else if (cfield instanceof ComputedField) {
+                    assignKey(cfield[Copy]());
+                    //   ComputedField.MakeFunction(cfield.script.originalScript));
+                } else if (field instanceof ObjectField) {
+                    await copyObjectField(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happen...
                 } else {
-                    if (field instanceof RefField) {
-                        assignKey(field);
-                    } else if (cfield instanceof ComputedField) {
-                        assignKey(cfield[Copy]());
-                        //   ComputedField.MakeFunction(cfield.script.originalScript));
-                    } else if (field instanceof ObjectField) {
-                        await copyObjectField(field);
-                    } else if (field instanceof Promise) {
-                        debugger; //This shouldn't happen...
-                    } else {
-                        assignKey(field);
-                    }
+                    assignKey(field);
                 }
             })
         );
@@ -807,7 +818,8 @@ export namespace Doc {
             };
             const regex = `(${Doc.localServerPath()})([^"]*)`;
             const re = new RegExp(regex, 'g');
-            copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+            const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g');
+            copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
         });
         return { clone: copy, map: cloneMap, linkMap };
     }
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index d7edd4266..3e75a071f 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -1,11 +1,11 @@
-import { serializable } from "serializr";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { Deserializable } from "../client/util/SerializationHelper";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { ObjectField } from "./ObjectField";
+import { serializable } from 'serializr';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { Deserializable } from '../client/util/SerializationHelper';
+import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { ObjectField } from './ObjectField';
 
 @scriptingGlobal
-@Deserializable("RichTextField")
+@Deserializable('RichTextField')
 export class RichTextField extends ObjectField {
     @serializable(true)
     readonly Data: string;
@@ -13,14 +13,14 @@ export class RichTextField extends ObjectField {
     @serializable(true)
     readonly Text: string;
 
-    constructor(data: string, text: string = "") {
+    constructor(data: string, text: string = '') {
         super();
         this.Data = data;
         this.Text = text;
     }
 
     Empty() {
-        return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align"));
+        return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align'));
     }
 
     [Copy]() {
@@ -28,14 +28,16 @@ export class RichTextField extends ObjectField {
     }
 
     [ToScriptString]() {
-        return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`;
+        return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`;
     }
     [ToString]() {
         return this.Text;
     }
 
     public static DashField(fieldKey: string) {
-        return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, "");
+        return new RichTextField(
+            `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docId":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`,
+            ''
+        );
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index bf055cd8b..239b59e83 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -264,18 +264,18 @@ export namespace RichTextUtils {
 
         const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
             const { url: src, width, agnostic } = image;
-            let docid: string;
+            let docId: string;
             const guid = Utils.GenerateDeterministicGuid(agnostic);
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
                 const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
                 DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
-                docid = backingDoc[Id];
-                textNote[guid] = docid;
+                docId = backingDoc[Id];
+                textNote[guid] = docId;
             } else {
-                docid = backingDocId;
+                docId = backingDocId;
             }
-            return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' });
+            return schema.node('image', { src, agnostic, width, docId, float: null, location: 'add:right' });
         };
 
         const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
-- 
cgit v1.2.3-70-g09d2


From fd71eb1a130058207b45cc6a1da0dbd97f6dd4f1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 24 Mar 2023 23:18:44 -0400
Subject: fixed showing keyValueBox when document opacity is 0 or it is hidden.
  fixed toggling link targets.  fixed sorting and undoing schema view changes.

---
 src/client/util/DocumentManager.ts                 |  1 +
 src/client/views/StyleProvider.tsx                 |  5 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  1 +
 .../collectionSchema/CollectionSchemaView.tsx      | 81 +++++++++++-----------
 .../collections/collectionSchema/SchemaRowBox.tsx  |  1 -
 src/client/views/nodes/DocumentView.tsx            |  9 ++-
 src/client/views/nodes/LabelBox.tsx                |  1 +
 8 files changed, 54 insertions(+), 47 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 947613801..ccf370662 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -262,6 +262,7 @@ export class DocumentManager {
         finished?: () => void
     ) => {
         const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
+        if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
         let rootContextView = await new Promise<DocumentView>(res => {
             const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
             if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index d1e85a65b..1b5eb3342 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -146,6 +146,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             return (
                 (doc &&
                     !doc.presentationTargetDoc &&
+                    !props?.LayoutTemplateString?.includes(KeyValueBox.name) &&
                     props?.showTitle?.() !== '' &&
                     StrCast(
                         doc._showTitle,
@@ -169,7 +170,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             if (!backColor) return undefined;
             return lightOrDark(backColor);
         case StyleProp.Hidden:
-            return BoolCast(doc?.hidden);
+            return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? false : BoolCast(doc?.hidden);
         case StyleProp.BorderRounding:
             return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : ''));
         case StyleProp.TitleHeight:
@@ -288,7 +289,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         }
         case StyleProp.PointerEvents:
             const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
-            if (MainView.Instance._exploreMode) return isInk ? 'visiblePainted' : 'all';
+            if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all';
             if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
             if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
             if (props?.pointerEvents?.() === 'none') return 'none';
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index bd74c9399..5100d8d67 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -79,7 +79,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 .map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc))
                 .filter(pair => {
                     // filter out any documents that have a proto that we don't have permissions to
-                    return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
+                    return !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
                 });
             return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
         }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4f81af95d..e7d1eeb90 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -319,6 +319,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     getView = async (doc: Doc): Promise<Opt<DocumentView>> => {
         return new Promise<Opt<DocumentView>>(res => {
+            doc.hidden && (doc.hidden = false);
             const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
             findDoc(dv => res(dv));
         });
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index ad31113a2..f5d3243f4 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -40,7 +40,6 @@ const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate',
 
 @observer
 export class CollectionSchemaView extends CollectionSubView() {
-    private _ref: HTMLDivElement | null = null;
     private _closestDropIndex: number = 0;
     private _previewRef: HTMLDivElement | null = null;
     private _makeNewColumn: boolean = false;
@@ -177,18 +176,6 @@ export class CollectionSchemaView extends CollectionSubView() {
     setSort = (field: string | undefined, desc: boolean = false) => {
         this.layoutDoc.sortField = field;
         this.layoutDoc.sortDesc = desc;
-
-        if (field === undefined) return;
-
-        this.childDocs.sort((docA, docB) => {
-            const aStr = Field.toString(docA[field] as Field);
-            const bStr = Field.toString(docB[field] as Field);
-            var out = 0;
-            if (aStr < bStr) out = -1;
-            if (aStr > bStr) out = 1;
-            if (desc) out *= -1;
-            return out;
-        });
     };
 
     addRow = (doc: Doc | Doc[]) => {
@@ -372,10 +359,9 @@ export class CollectionSchemaView extends CollectionSubView() {
         const draggedDocs = de.complete.docDragData?.draggedDocuments;
         if (draggedDocs && super.onInternalDrop(e, de)) {
             const pushedDocs = this.childDocs.filter((doc, index) => index >= this._closestDropIndex && !draggedDocs.includes(doc));
-            this.props.removeDocument?.(pushedDocs);
-            this.props.removeDocument?.(draggedDocs);
-            this.addDocument(draggedDocs);
-            this.addDocument(pushedDocs);
+            const pushedAndDraggedDocs = [...pushedDocs, ...draggedDocs];
+            const removed = this.childDocs.slice().filter(doc => !pushedAndDraggedDocs.includes(doc));
+            this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...removed, ...draggedDocs, ...pushedDocs]);
             this.setSort(undefined);
             SelectionManager.DeselectAll();
             setTimeout(() => draggedDocs.forEach(doc => DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true))), 100);
@@ -804,16 +790,30 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
 
+    @computed get sortedDocs() {
+        const field = StrCast(this.layoutDoc.sortField);
+        const desc = BoolCast(this.layoutDoc.sortDesc);
+        return !field
+            ? this.childDocs
+            : this.childDocs.sort((docA, docB) => {
+                  const aStr = Field.toString(docA[field] as Field);
+                  const bStr = Field.toString(docB[field] as Field);
+                  var out = 0;
+                  if (aStr < bStr) out = -1;
+                  if (aStr > bStr) out = 1;
+                  if (desc) out *= -1;
+                  return out;
+              });
+    }
+    sortedDocsFunc = () => this.sortedDocs;
     isContentActive = () => this.props.isSelected() || this.props.isContentActive();
     screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0);
     previewWidthFunc = () => this.previewWidth;
     render() {
-        trace();
         return (
             <div
                 className="collectionSchemaView"
                 ref={(ele: HTMLDivElement | null) => {
-                    this._ref = ele;
                     this.createDashEventsTarget(ele);
                 }}
                 onPointerDown={e => {
@@ -834,28 +834,26 @@ export class CollectionSchemaView extends CollectionSubView() {
                                 <FontAwesomeIcon icon="plus" />
                             </div>
                         </div>
-                        {this.columnKeys.map((key, index) => {
-                            return (
-                                <SchemaColumnHeader
-                                    key={index}
-                                    columnIndex={index}
-                                    columnKeys={this.columnKeys}
-                                    columnWidths={this.displayColumnWidths}
-                                    sortField={this.sortField}
-                                    sortDesc={this.sortDesc}
-                                    setSort={this.setSort}
-                                    removeColumn={this.removeColumn}
-                                    resizeColumn={this.startResize}
-                                    openContextMenu={this.openContextMenu}
-                                    dragColumn={this.dragColumn}
-                                    setColRef={this.setColRef}
-                                />
-                            );
-                        })}
+                        {this.columnKeys.map((key, index) => (
+                            <SchemaColumnHeader
+                                key={index}
+                                columnIndex={index}
+                                columnKeys={this.columnKeys}
+                                columnWidths={this.displayColumnWidths}
+                                sortField={this.sortField}
+                                sortDesc={this.sortDesc}
+                                setSort={this.setSort}
+                                removeColumn={this.removeColumn}
+                                resizeColumn={this.startResize}
+                                openContextMenu={this.openContextMenu}
+                                dragColumn={this.dragColumn}
+                                setColRef={this.setColRef}
+                            />
+                        ))}
                     </div>
                     {this._columnMenuIndex !== undefined && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
-                    <CollectionSchemaViewDocs schema={this} />
+                    <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} sortField={StrCast(this.layoutDoc.sortField)} sortDesc={BoolCast(this.layoutDoc.sortDesc)} />
 
                     <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
                 </div>
@@ -901,6 +899,9 @@ export class CollectionSchemaView extends CollectionSubView() {
 
 interface CollectionSchemaViewDocsProps {
     schema: CollectionSchemaView;
+    childDocs: () => Doc[];
+    sortField: string; // I don't know why these are needed since the childDocs function changes when the sort changes.  However, for some reason that doesn't cause a re-render...
+    sortDesc: boolean;
 }
 
 @observer
@@ -910,8 +911,8 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
     render() {
         return (
             <div className="schema-table-content">
-                {this.props.schema.childDocs.map((doc: Doc, index: number) => {
-                    const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.schema.props.DataDoc;
+                {this.props.childDocs().map((doc: Doc, index: number) => {
+                    const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.schema.props.DataDoc;
                     return (
                         <div className="schema-row-wrapper" style={{ maxHeight: CollectionSchemaView._rowHeight }}>
                             <DocumentView
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 0c8c0ee59..10f307068 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -50,7 +50,6 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     onPointerEnter = (e: any) => {
-        //if (!this.schemaView?._isDragging) return;
         if (!SnappingManager.GetIsDragging()) return;
         document.removeEventListener('pointermove', this.onPointerMove);
         document.addEventListener('pointermove', this.onPointerMove);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1f717932e..42c2b28ba 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -332,7 +332,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         return this.props.NativeHeight();
     }
     @computed get onClickHandler() {
-        const noClick = this.rootSelected() || this.props.isSelected() || this._componentView?.isAnyChildContentActive?.();
+        const noClick = this.props.LayoutTemplateString?.includes(KeyValueBox.name) || this.rootSelected() || this.props.isSelected() || this._componentView?.isAnyChildContentActive?.();
         return noClick ? undefined : this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
     }
     @computed get onDoubleClickHandler() {
@@ -1483,7 +1483,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         });
 
         const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc);
-        return (
+        return this.hidden ? null : (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
                 ref={this._mainCont}
@@ -1640,6 +1640,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
         return hideCount ? null : <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
     }
+    @computed get hidden() {
+        return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden);
+    }
 
     @computed get docViewPath(): DocumentView[] {
         return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this];
@@ -1826,7 +1829,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
         const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
 
-        return (
+        return this.hidden ? null : (
             <div
                 className="contentFittingDocumentView"
                 onPointerEnter={action(() => {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 6e0b4be37..916458dfd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -131,6 +131,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
                     style={{
                         backgroundColor: this.hoverColor,
                         fontSize: StrCast(this.layoutDoc._fontSize),
+                        color: StrCast(this.layoutDoc._color),
                         fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit',
                         letterSpacing: StrCast(this.layoutDoc.letterSpacing),
                         textTransform: StrCast(this.layoutDoc.textTransform) as any,
-- 
cgit v1.2.3-70-g09d2


From e4b76f6c1dee63c49408c48d7b2bc98e62dc813a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 5 Apr 2023 12:19:01 -0400
Subject: adjusted general context menu items to always appear last.  cleaned
 up makeLink api,

---
 src/client/documents/Documents.ts                  | 32 +++++-----
 src/client/util/DragManager.ts                     |  1 -
 src/client/util/LinkFollower.ts                    |  2 -
 src/client/views/ContextMenu.tsx                   | 12 ++--
 src/client/views/DocumentButtonBar.tsx             |  4 +-
 src/client/views/MainView.tsx                      |  1 +
 src/client/views/SidebarAnnos.tsx                  |  2 +-
 .../views/collections/CollectionNoteTakingView.tsx |  2 +-
 .../views/collections/CollectionStackingView.tsx   | 28 ++++-----
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/collections/TreeView.tsx          |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 26 ++------
 .../collections/collectionFreeForm/MarqueeView.tsx |  2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 70 +++++++++++++++-------
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/client/views/nodes/PDFBox.tsx                  | 31 +++++-----
 src/client/views/nodes/VideoBox.tsx                |  6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 src/client/views/search/SearchBox.tsx              |  2 +-
 21 files changed, 117 insertions(+), 118 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9cb480c4a..5623ca218 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -470,7 +470,7 @@ export namespace Docs {
                 DocumentType.AUDIO,
                 {
                     layout: { view: AudioBox, dataField: defaultDataKey },
-                    options: { _height: 100, forceReflow: true, nativeDimModifiable: true },
+                    options: { _height: 100, fitWidth: true, forceReflow: true, nativeDimModifiable: true },
                 },
             ],
             [
@@ -934,13 +934,13 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
         }
 
-        export function LinkDocument(source: { doc: Doc; ctx?: Doc }, target: { doc: Doc; ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
+        export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
             const linkDoc = InstanceFromProto(
                 Prototypes.get(DocumentType.LINK),
                 undefined,
                 {
-                    anchor1: source.doc,
-                    anchor2: target.doc,
+                    anchor1: source,
+                    anchor2: target,
                     ...options,
                 },
                 id
@@ -1294,16 +1294,14 @@ export namespace DocUtils {
         broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
         return DocUtils.ActiveRecordings.map(audio => {
             const sourceDoc = getSourceDoc();
-            const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor(true) || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
-            link && (link.followLinkLocation = OpenWhere.addRight);
-            return link;
+            return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { linkDisplay: false, linkRelationship: 'recording annotation:linked recording', description: 'recording timeline' });
         });
     }
 
-    export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = '', description: string = '', id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
-        if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
-        const sv = DocumentManager.Instance.getDocumentView(source.doc);
-        if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return;
+    export function MakeLink(source: Doc, target: Doc, linkSettings: { linkRelationship?: string; description?: string; linkDisplay?: boolean }, id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
+        if (!linkSettings.linkRelationship) linkSettings.linkRelationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
+        const sv = DocumentManager.Instance.getDocumentView(source);
+        if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target) return;
         if (target.doc === Doc.UserDoc()) return undefined;
 
         const makeLink = action((linkDoc: Doc, showPopup?: number[]) => {
@@ -1343,16 +1341,16 @@ export namespace DocUtils {
                 target,
                 {
                     title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
-                    'anchor1-useLinkSmallAnchor': source.doc.useLinkSmallAnchor ? true : undefined,
-                    'anchor2-useLinkSmallAnchor': target.doc.useLinkSmallAnchor ? true : undefined,
+                    'anchor1-useLinkSmallAnchor': source.useLinkSmallAnchor ? true : undefined,
+                    'anchor2-useLinkSmallAnchor': target.useLinkSmallAnchor ? true : undefined,
                     'acl-Public': SharingPermissions.Augment,
                     '_acl-Public': SharingPermissions.Augment,
-                    linkDisplay: true,
+                    linkDisplay: linkSettings.linkDisplay,
                     _linkAutoMove: true,
-                    linkRelationship,
+                    linkRelationship: linkSettings.linkRelationship,
                     _showCaption: 'description',
                     _showTitle: 'linkRelationship',
-                    description,
+                    description: linkSettings.description,
                 },
                 id
             ),
@@ -1700,7 +1698,7 @@ export namespace DocUtils {
                 _timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
             });
             Doc.AddDocToList(context, annotationField, pushpin);
-            const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, 'pushpin', '');
+            const pushpinLink = DocUtils.MakeLink(pushpin, doc, { linkRelationship: 'pushpin' }, '');
             doc._timecodeToShow = undefined;
             return pushpin;
         }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index cc116fb46..7d2aa813f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -168,7 +168,6 @@ export namespace DragManager {
             this.dropDocCreator = dropDocCreator;
             this.offset = [0, 0];
         }
-        linkSourceDoc?: Doc;
         dropDocCreator: (annotationOn: Doc | undefined) => Doc;
         dropDocument?: Doc;
         offset: number[];
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 785018990..df61ecece 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -9,8 +9,6 @@ import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
 /*
  * link doc:
  * - anchor1: doc
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 6a530e3ae..e4c3e864b 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -102,13 +102,11 @@ export class ContextMenu extends React.Component {
         }
     }
     @action
-    moveAfter(item: ContextMenuProps, after: ContextMenuProps) {
-        if (after && this.findByDescription(after.description)) {
-            const curInd = this._items.findIndex(i => i.description === item.description);
-            this._items.splice(curInd, 1);
-            const afterInd = this._items.findIndex(i => i.description === after.description);
-            this._items.splice(afterInd + 1, 0, item);
-        }
+    moveAfter(item: ContextMenuProps, after?: ContextMenuProps) {
+        const curInd = this._items.findIndex(i => i.description === item.description);
+        this._items.splice(curInd, 1);
+        const afterInd = after && this.findByDescription(after.description) ? this._items.findIndex(i => i.description === after.description) : this._items.length;
+        this._items.splice(afterInd, 0, item);
     }
     @action
     setDefaultItem(prefix: string, item: (name: string) => void) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 9389fed01..3e1a0f265 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -416,7 +416,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         const targetDoc = this.view0?.props.Document;
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
-                <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}>
+                <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={this.openContextMenu}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
                 </div>
             </Tooltip>
@@ -569,7 +569,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc;
             const trail = DocCast(anchor.presTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true);
             if (trail !== anchor.presTrail) {
-                DocUtils.MakeLink({ doc: anchor }, { doc: trail }, 'link trail');
+                DocUtils.MakeLink(anchor, trail, { linkRelationship: 'link trail' });
                 anchor.presTrail = trail;
             }
             Doc.ActivePresentation = trail;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index befb0200d..b0888df68 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -270,6 +270,7 @@ export class MainView extends React.Component {
                 fa.faLaptopCode,
                 fa.faMale,
                 fa.faCopy,
+                fa.faHome,
                 fa.faHandPointLeft,
                 fa.faHandPointRight,
                 fa.faCompass,
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 48c0150e6..2d2b0f83e 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -74,7 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         });
         FormattedTextBox.SelectOnLoad = target[Id];
         FormattedTextBox.DontSelectInitialText = true;
-        const link = DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+        const link = DocUtils.MakeLink(anchor, target, { linkRelationship: 'inline comment:comment on' });
         link && (link.linkDisplay = false);
 
         const taggedContent = this.docFilters()
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 121260680..cb5be990d 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -441,7 +441,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
             this.props.addDocument?.(source);
-            de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+            de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed
             e.stopPropagation();
         } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
         return false;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 64ec419ec..1e02fc9d4 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -3,14 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CursorProperty } from 'csstype';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -460,7 +460,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
             this.props.addDocument?.(source);
-            de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+            de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed
             e.stopPropagation();
         } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
         return false;
@@ -623,11 +623,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
         if (!e.isPropagationStopped()) {
-            const subItems: ContextMenuProps[] = [];
-            subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
-            subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
-            subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
-            ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
+            const cm = ContextMenu.Instance;
+            const options = cm.findByDescription('Options...');
+            const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+            optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+            optionItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+            optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' });
+            !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
         }
     };
 
@@ -735,14 +737,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                                 <EditableView {...editableViewProps} />
                             </div>
                         )}
-                        {/* {this.chromeHidden || !this.props.isSelected() ? (null) :
-                            <Switch
-                                onChange={this.onToggle}
-                                onClick={this.onToggle}
-                                defaultChecked={true}
-                                checkedChildren="edit"
-                                unCheckedChildren="view"
-                            />} */}
                     </div>
                 </div>
             </>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5100d8d67..132ed6fb6 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -335,7 +335,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                                 const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
                                 if (focusNode) {
                                     const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
-                                    anchor && DocUtils.MakeLink({ doc: htmlDoc }, { doc: anchor });
+                                    anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
                                 }
                             }
                         }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 99b7549c0..75e76019e 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -359,7 +359,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         if (de.complete.linkDragData) {
             const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
             const destDoc = this.doc;
-            DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, 'tree link', '');
+            DocUtils.MakeLink(sourceDoc, destDoc, { linkRelationship: 'tree link' });
             e.stopPropagation();
         }
         const docDragData = de.complete.docDragData;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d39668a5d..fb0bd2a19 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -397,7 +397,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document
                 const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' });
                 this.props.addDocument?.(source);
-                de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, 'annotated by:annotation of', ''); // TODODO this is where in text links get passed
+                de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { linkRelationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed
             }
             e.stopPropagation(); //  do nothing if link is dropped into any freeform view parent of dragged document
             return true;
@@ -1693,6 +1693,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             },
             icon: 'compress-arrows-alt',
         });
+        if (this.props.setContentView === emptyFunction) {
+            !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
+            return;
+        }
         !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' });
         appearanceItems.push({
             description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`,
@@ -1700,7 +1704,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt',
         });
         appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' });
-        //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
+        !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
         appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
         this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' });
@@ -1730,27 +1734,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
         const mores = ContextMenu.Instance.findByDescription('More...');
         const moreItems = mores && 'subitems' in mores ? mores.subitems : [];
-        moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
         !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
     };
 
-    importDocument = (x: number, y: number) => {
-        const input = document.createElement('input');
-        input.type = 'file';
-        input.accept = '.zip';
-        input.onchange = _e => {
-            input.files &&
-                Doc.importDocument(input.files[0]).then(doc => {
-                    if (doc instanceof Doc) {
-                        const [xx, yy] = this.getTransform().transformPoint(x, y);
-                        (doc.x = xx), (doc.y = yy);
-                        this.props.addDocument?.(doc);
-                    }
-                });
-        };
-        input.click();
-    };
-
     @undoBatch
     @action
     transcribeStrokes = (math: boolean) => {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 043fe0bcf..0b4f76fa1 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -527,7 +527,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         });
         const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
         const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
-        DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', '');
+        DocUtils.MakeLink(summary, portal, { linkRelationship: 'summary of:summarized by' });
 
         portal.hidden = true;
         this.props.addDocument?.(portal);
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d6679b46d..df3299eef 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -142,7 +142,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
             } else if (startLink !== endLink) {
                 endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink;
                 startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
-                const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
+                const linkDoc = DocUtils.MakeLink(startLink, endLink, { linkRelationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }, undefined, true);
 
                 LinkManager.currentLink = linkDoc;
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c7bf37a45..159aa026a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -617,22 +617,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    drop = async (e: Event, de: DragManager.DropEvent) => {
+    drop = (e: Event, de: DragManager.DropEvent) => {
         if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
         if (this.props.Document === Doc.ActiveDashboard) {
             alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.');
             return;
         }
         const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
-        if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
-        if (linkdrag?.linkSourceDoc) {
-            e.stopPropagation();
-            if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
-                de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
-            }
-            if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
-                const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document;
-                de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
+        if (linkdrag) {
+            linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
+            if (linkdrag.linkSourceDoc) {
+                e.stopPropagation();
+                if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
+                    de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
+                }
+                if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
+                    const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc;
+                    de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, undefined, [de.x, de.y - 50]);
+                }
             }
         }
     };
@@ -643,9 +645,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document && d.linkRelationship === 'portal to:portal from');
         if (!portalLink) {
             DocUtils.MakeLink(
-                { doc: this.props.Document },
-                { doc: Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }) },
-                'portal to:portal from'
+                this.props.Document,
+                Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }),
+                { linkRelationship: 'portal to:portal from' }
             );
         }
         this.Document.followLinkLocation = OpenWhere.lightbox;
@@ -653,6 +655,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         this.Document._isLinkButton = true;
     };
 
+    importDocument = () => {
+        const input = document.createElement('input');
+        input.type = 'file';
+        input.accept = '.zip';
+        input.onchange = _e => {
+            if (input.files) {
+                const batch = UndoManager.StartBatch('importing');
+                Doc.importDocument(input.files[0]).then(doc => {
+                    if (doc instanceof Doc) {
+                        this.props.addDocTab(doc, OpenWhere.addRight);
+                        batch.end();
+                    }
+                });
+            }
+        };
+        input.click();
+    };
+
     @action
     onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
         if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) {
@@ -704,9 +724,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const appearance = cm.findByDescription('UI Controls...');
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
-            LinkManager.Links(this.Document).length &&
-                appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
-            !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
+            !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
             if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
                 const existingOnClick = cm.findByDescription('OnClick...');
@@ -774,16 +792,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     }
                     moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' });
                 }
-                moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
-
-                (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
             }
 
-            if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
+            !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+        }
+        const constantItems: ContextMenuProps[] = [];
+
+        if (!Doc.IsSystem(this.rootDoc)) {
+            constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
+            constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
+            (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
+            if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
-                moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
+                constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
             }
-            !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+            cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' });
         }
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
@@ -834,7 +857,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 icon: 'book',
             });
         }
-        cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+        if (!help) cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+        else cm.moveAfter(help);
 
         e?.stopPropagation(); // DocumentViews should stop propagation of this event
         cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b5193cd20..f38ebba27 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -222,7 +222,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.panYMin = anchy / viewScale;
         croppingProto.panYMax = anchh / viewScale;
         if (addCrop) {
-            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
             cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
             cropping.y = NumCast(this.rootDoc.y);
             this.props.addDocTab(cropping, OpenWhere.inParent);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index a254edd87..e38d7e2a8 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -130,7 +130,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         croppingProto['data-nativeWidth'] = anchw;
         croppingProto['data-nativeHeight'] = anchh;
         if (addCrop) {
-            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
         }
         this.props.bringToFront(cropping);
 
@@ -344,7 +344,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             },
             (e, movement, isClick) => !isClick && batch.end(),
             () => {
-                this.toggleSidebar();
+                onButton && this.toggleSidebar();
                 batch.end();
             }
         );
@@ -440,17 +440,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
         return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
     };
+    @undoBatch
+    toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
     specificContextMenu = (e: React.MouseEvent): void => {
-        const funcs: ContextMenuProps[] = [];
-        funcs.push({
-            description: 'Toggle Sidebar Type',
-            event: () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform),
-            icon: 'expand-arrows-alt',
-        });
-        funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
-        funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
-        //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
-        ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+        const cm = ContextMenu.Instance;
+        const options = cm.findByDescription('Options...');
+        const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+        optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' });
+        !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
+        //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
+        !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
+        const help = cm.findByDescription('Help...');
+        const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+        helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
+        !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' });
     };
 
     @computed get renderTitleBox() {
@@ -523,8 +526,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     sidebarAddDocument={this.sidebarAddDocument}
                     moveDocument={this.moveDocument}
                     removeDocument={this.removeDocument}
-                    //PanelWidth={this.sidebarWidth}
-                    //ScreenToLocalTransform={this.sidebarScreenToLocal}
                 />
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
@@ -568,7 +569,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
         return !this._pdf ? null : (
             <div
-                className={'pdfBox'}
+                className="pdfBox"
                 onContextMenu={this.specificContextMenu}
                 style={{
                     display: this.props.thumbShown?.() ? 'none' : undefined,
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0570c7fcb..e14ad4b05 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -327,7 +327,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 _isLinkButton: true,
             });
             this.props.addDocument?.(b);
-            DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+            DocUtils.MakeLink(b, this.rootDoc, { linkRelationship: 'video snapshot' });
             Networking.PostToServer('/youtubeScreenshot', {
                 id: this.youtubeVideoId,
                 timecode: this.layoutDoc._currentTimecode,
@@ -376,7 +376,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc));
         Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc));
         this.props.addDocument?.(imageSnapshot);
-        const link = DocUtils.MakeLink({ doc: imageSnapshot }, { doc: this.getAnchor(true) }, 'video snapshot');
+        const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { linkRelationship: 'video snapshot' });
         link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
         setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
     };
@@ -1031,7 +1031,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.panYMin = anchy / viewScale;
         croppingProto.panYMax = anchh / viewScale;
         if (addCrop) {
-            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
         }
         this.props.bringToFront(cropping);
         return cropping;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d8afc3227..361e000f9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -454,7 +454,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                             alink =
                                 alink ??
                                 (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
-                                    DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
+                                    DocUtils.MakeLink(this.props.Document, target, { linkRelationship: LinkManager.AutoKeywords })!);
                             newAutoLinks.add(alink);
                             const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
                             allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
@@ -1273,7 +1273,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         ]),
                     ]);
 
-                    const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted');
+                    const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { linkRelationship: 'PDF pasted' });
                     if (link) {
                         view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
                     }
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 9e9b61db3..e691869cc 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -259,7 +259,7 @@ export class RichTextRules {
                                 this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
                             }
                             const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId);
-                            DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
+                            DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' });
 
                             const fstate = this.TextBox.EditorView?.state;
                             if (fstate && selection) {
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index fe9b13fbe..8f93f1150 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -117,7 +117,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
     makeLink = action((linkTo: Doc) => {
         const linkFrom = this.props.linkCreateAnchor?.();
         if (linkFrom) {
-            const link = DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
+            const link = DocUtils.MakeLink(linkFrom, linkTo, {});
             link && this.props.linkCreated?.(link);
         }
     });
-- 
cgit v1.2.3-70-g09d2

