From 11b6cb8344f26e5895bebc3dedef491675717e30 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 5 Mar 2024 15:11:29 -0500
Subject: fixed text in templates to revert to template value when noTemplate
 is set. made dashFieldView read from datadoc.  added margins for
 multicolumn/row, enabled chromHidden for multicolumn/row

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

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

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e4e7bec32..cbd3ff358 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -135,7 +135,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return this.selectedDoc?.isGroup;
     }
     @computed get isStack() {
-        return [CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as any);
+        return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as any);
     }
 
     rtfWidth = () => (!this.selectedLayoutDoc ? 0 : Math.min(NumCast(this.selectedLayoutDoc?._width), this._props.width - 20));
@@ -1076,6 +1076,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             <div className="transform-editor">
                 {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), (val: number) => !isNaN(val) && (this.selectedDoc!.gridGap = val))}
                 {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), (val: number) => !isNaN(val) && (this.selectedDoc!.xMargin = val))}
+                {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), (val: number) => !isNaN(val) && (this.selectedDoc!.yMargin = val))}
                 {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), (val: number) => !isNaN(val) && (this.selectedDoc!.xPadding = this.selectedDoc!.yPadding = val))}
                 {this.isInk ? this.controlPointsButton : null}
                 {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, (val: number) => !isNaN(val) && (this.shapeWid = val), 1000, 1)}
-- 
cgit v1.2.3-70-g09d2


From b420caf2c7ecd386cae2cc550904522474b541aa Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 26 Mar 2024 22:34:10 -0400
Subject: added empty image tool and click on empty image to select from
 filesystem.   fixed following links in lightbox and showing links to
 stackedTimelines.  fixed embedding docs into text.  fixed not resizing text
 boxes that also show up in pivot view.  prevent context menu from going off
 top of screen.   fixed freeform clustering colors and click to type. fixed
 links to stackedTimeline marks, and titles for marks.  made title editing
 from doc deco and header use same syntax as keyValue.  fixed marquee
 selection on webBoxes.  turn off transitions in freeformdocview after
 timeout. enabled iconifying templates to propagate to "offspring".  fixes
 images in templates.  don't show headr on schema views.

---
 src/client/documents/Documents.ts                  | 52 ++++++++--------
 src/client/util/DocumentManager.ts                 |  4 +-
 src/client/views/ContextMenu.tsx                   |  6 +-
 src/client/views/DocumentDecorations.tsx           | 71 ++++++++--------------
 src/client/views/FieldsDropdown.tsx                |  1 -
 src/client/views/LightboxView.tsx                  |  2 +
 src/client/views/MarqueeAnnotator.tsx              | 19 ++++--
 src/client/views/ObservableReactComponent.tsx      |  3 +-
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/StyleProvider.tsx                 |  6 +-
 .../collections/CollectionStackedTimeline.tsx      | 13 ++--
 .../CollectionFreeFormLayoutEngines.tsx            |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 11 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 .../collectionSchema/SchemaTableCell.tsx           |  2 +-
 src/client/views/global/globalScripts.ts           |  4 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 24 ++++++--
 src/client/views/nodes/DocumentContentsView.tsx    |  7 ++-
 src/client/views/nodes/DocumentView.tsx            | 35 +++++++----
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/ImageBox.tsx                | 55 +++++++++++++----
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 src/client/views/nodes/LabelBox.tsx                |  2 +-
 src/client/views/nodes/LinkBox.tsx                 | 47 ++++++++++----
 src/client/views/nodes/VideoBox.tsx                |  5 +-
 src/client/views/nodes/WebBox.tsx                  | 65 ++++++++++++++------
 .../views/nodes/formattedText/FormattedTextBox.tsx | 17 ++++--
 src/client/views/nodes/trails/PresBox.tsx          |  2 +-
 src/fields/Doc.ts                                  | 29 +++++----
 src/fields/PresField.ts                            |  6 --
 src/fields/RichTextField.ts                        |  7 ---
 31 files changed, 311 insertions(+), 195 deletions(-)
 delete mode 100644 src/fields/PresField.ts

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 17cb6fef8..b63c5e429 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1017,8 +1017,8 @@ export namespace Docs {
         }
 
         export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
-            const imgField = url instanceof ImageField ? url : new ImageField(url);
-            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
+            const imgField = url instanceof ImageField ? url : url ? new ImageField(url) : undefined;
+            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField?.url.href ?? '-no image-'), ...options }, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function PresDocument(options: DocumentOptions = {}) {
@@ -1950,6 +1950,31 @@ export namespace DocUtils {
         return dd;
     }
 
+    export function assignImageInfo(result: Upload.FileInformation, proto: Doc) {
+        if (Upload.isImageInformation(result)) {
+            const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim);
+            const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase();
+            proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
+            proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+            proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+            if (NumCast(proto['data-nativeOrientation']) >= 5) {
+                proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+                proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+            }
+            proto.data_exif = JSON.stringify(result.exifData?.data);
+            proto.data_contentSize = result.contentSize;
+            // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates
+            const latitude = result.exifData?.data?.GPSLatitude;
+            const latitudeDirection = result.exifData?.data?.GPSLatitudeRef;
+            const longitude = result.exifData?.data?.GPSLongitude;
+            const longitudeDirection = result.exifData?.data?.GPSLongitudeRef;
+            if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) {
+                proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection);
+                proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
+            }
+        }
+    }
+
     async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, overwriteDoc?: Doc) {
         if (result instanceof Error) {
             alert(`Upload failed: ${result.message}`);
@@ -1961,28 +1986,7 @@ export namespace DocUtils {
         if (doc) {
             const proto = Doc.GetProto(doc);
             proto.text = result.rawText;
-            if (Upload.isImageInformation(result)) {
-                const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim);
-                const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase();
-                proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
-                proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
-                proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
-                if (NumCast(proto['data-nativeOrientation']) >= 5) {
-                    proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
-                    proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
-                }
-                proto.data_exif = JSON.stringify(result.exifData?.data);
-                proto.data_contentSize = result.contentSize;
-                // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates
-                const latitude = result.exifData?.data?.GPSLatitude;
-                const latitudeDirection = result.exifData?.data?.GPSLatitudeRef;
-                const longitude = result.exifData?.data?.GPSLongitude;
-                const longitudeDirection = result.exifData?.data?.GPSLongitudeRef;
-                if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) {
-                    proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection);
-                    proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
-                }
-            }
+            !(result instanceof Error) && DocUtils.assignImageInfo(result, proto);
             if (Upload.isVideoInformation(result)) {
                 proto.data_duration = result.duration;
             }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index a38a330da..40d28c690 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -59,7 +59,7 @@ export class DocumentManager {
     private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
     public AddViewRenderedCb = (doc: Opt<Doc>, func: (dv: DocumentView) => any) => {
         if (doc) {
-            const dv = this.getDocumentView(doc);
+            const dv = LightboxView.LightboxDoc ? this.getLightboxDocumentView(doc) : this.getDocumentView(doc);
             this._viewRenderedCbs.push({ doc, func });
             if (dv) {
                 this.callAddViewFuncs(dv);
@@ -262,7 +262,7 @@ export class DocumentManager {
                     return res(this.getDocumentView(docContextPath[0])!);
                 }
                 options.didMove = true;
-                docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+                (!LightboxView.LightboxDoc && docContextPath.some(doc => TabDocView.Activate(doc))) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
                 this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
             }));
         if (options.openLocation === OpenWhere.lightbox) {
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 8f4e43978..ca877b93e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -218,11 +218,12 @@ export class ContextMenu extends ObservableReactComponent<{}> {
                         this._width = DivWidth(r);
                         this._height = DivHeight(r);
                     }
+                    this._searchRef.current?.focus();
                 })}
                 style={{
                     display: this._display ? '' : 'none',
                     left: this.pageX,
-                    ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }),
+                    ...(this._yRelativeToTop ? { top: Math.max(0, this.pageY) } : { bottom: this.pageY }),
                     background: SettingsManager.userBackgroundColor,
                     color: SettingsManager.userColor,
                 }}>
@@ -265,7 +266,8 @@ export class ContextMenu extends ObservableReactComponent<{}> {
             const item = this.flatItems[this._selectedIndex];
             if (item) {
                 item.event({ x: this.pageX, y: this.pageY });
-            } else if (this._searchString.startsWith(this._defaultPrefix)) {
+            } else {
+                //if (this._searchString.startsWith(this._defaultPrefix)) {
                 this._defaultItem?.(this._searchString.substring(this._defaultPrefix.length));
             }
             this.closeMenu();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2fb9f0fc1..4d9b93896 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -34,6 +34,7 @@ import { Colors } from './global/globalEnums';
 import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { KeyValueBox } from './nodes/KeyValueBox';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -57,7 +58,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     @observable _showNothing = true;
     @observable private _accumulatedTitle = '';
-    @observable private _titleControlString: string = '#title';
+    @observable private _titleControlString: string = '$title';
     @observable private _editingTitle = false;
     @observable private _hidden = false;
     @observable private _isRotating: boolean = false;
@@ -111,9 +112,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
 
     @action
     titleBlur = () => {
-        if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) {
+        if (this._accumulatedTitle.startsWith('$')) {
             this._titleControlString = this._accumulatedTitle;
-        } else if (this._titleControlString.startsWith('#')) {
+        } else if (this._titleControlString.startsWith('$')) {
             if (this._accumulatedTitle.startsWith('-->#')) {
                 SelectionManager.Docs.forEach(doc => (doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`)));
             }
@@ -131,26 +132,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                                 Doc.AddToMyPublished(d.Document);
                             }
                         }
-                        //@ts-ignore
-                        const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
-
-                        if (titleField.toString().startsWith('<this>')) {
-                            const title = titleField.toString().replace(/<this>\.?/, '');
-                            const curKey = Doc.LayoutFieldKey(d.Document);
-                            if (curKey !== title) {
-                                if (title) {
-                                    if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') {
-                                        d.Document.layout_fieldKey = `layout_${title}`;
-                                        d.Document[`layout_${title}`] = FormattedTextBox.LayoutString(title);
-                                        d.Document[`${title}_nativeWidth`] = d.Document[`${title}_nativeHeight`] = 0;
-                                    }
-                                } else {
-                                    d.Document.layout_fieldKey = undefined;
-                                }
-                            }
-                        } else {
-                            Doc.SetInPlace(d.Document, titleFieldKey, titleField, true);
-                        }
+                        KeyValueBox.SetField(d.Document, titleFieldKey, this._accumulatedTitle);
                     }),
                 'edit title'
             );
@@ -181,7 +163,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                 e => this.onBackgroundMove(true, e),
                 emptyFunction,
                 action(e => {
-                    !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString);
+                    const selected = SelectionManager.Views.length === 1 ? SelectionManager.Docs[0] : undefined;
+                    !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString);
                     this._editingTitle = true;
                     this._keyinput.current && setTimeout(this._keyinput.current.focus);
                 })
@@ -622,11 +605,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
     get selectionTitle(): string {
         if (SelectionManager.Views.length === 1) {
             const selected = SelectionManager.Views[0];
-            if (this._titleControlString.startsWith('=')) {
-                return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })?.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || '';
-            }
-            if (this._titleControlString.startsWith('#')) {
-                return Field.toString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-';
+            if (this._titleControlString.startsWith('$')) {
+                return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-';
             }
             return this._accumulatedTitle;
         }
@@ -731,21 +711,24 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             ) : null;
 
         const titleArea = this._editingTitle ? (
-            <input
-                ref={this._keyinput}
-                className="documentDecorations-title"
-                type="text"
-                name="dynbox"
-                autoComplete="on"
-                value={hideTitle ? '' : this._accumulatedTitle}
-                onBlur={action((e: React.FocusEvent) => {
-                    this._editingTitle = false;
-                    !hideTitle && this.titleBlur();
-                })}
-                onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))}
-                onKeyDown={hideTitle ? emptyFunction : this.titleEntered}
-                onPointerDown={e => e.stopPropagation()}
-            />
+            <>
+                {r - x < 150 ? null : <span>{this._titleControlString + ':'}</span>}
+                <input
+                    ref={this._keyinput}
+                    className="documentDecorations-title"
+                    type="text"
+                    name="dynbox"
+                    autoComplete="on"
+                    value={hideTitle ? '' : this._accumulatedTitle}
+                    onBlur={action((e: React.FocusEvent) => {
+                        this._editingTitle = false;
+                        !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">
                 {hideTitle ? null : (
diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx
index 5638d34c6..6a5c2cb4c 100644
--- a/src/client/views/FieldsDropdown.tsx
+++ b/src/client/views/FieldsDropdown.tsx
@@ -61,7 +61,6 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
             .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0]));
         const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet }));
 
-        console.log(options);
         return (
             <Select
                 styles={{
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index c7ff7ce47..b9d805145 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -23,6 +23,7 @@ import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { SnappingManager } from '../util/SnappingManager';
+import { Id } from '../../fields/FieldSymbols';
 
 interface LightboxViewProps {
     PanelWidth: number;
@@ -247,6 +248,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                     }}>
                     <GestureOverlay isActive={true}>
                         <DocumentView
+                            key={this._doc.title + this._doc[Id]} // this makes a new DocumentView when the document changes which makes link following work, otherwise no DocView is registered for the new Doc
                             ref={action((r: DocumentView | null) => (this._docView = r !== null ? r : undefined))}
                             Document={this._doc}
                             PanelWidth={this.lightboxWidth}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index bd6be2519..c29474fcd 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -34,7 +34,7 @@ export interface MarqueeAnnotatorProps {
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
     getPageFromScroll?: (top: number) => number;
-    finishMarquee: (x?: number, y?: number, PointerEvent?: PointerEvent) => void;
+    finishMarquee: (x?: number, y?: number) => void;
     anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
     anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
     highlightDragSrcColor?: string;
@@ -168,7 +168,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
 
     @action
     public onInitiateSelection(down: number[]) {
-        console.log('DOWN = ' + down[0] + ' ' + down[1]);
         this._width = this._height = 0;
         this._start = this.getTransformedScreenPt(down);
 
@@ -240,6 +239,13 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         document.removeEventListener('pointerup', this.onSelectEnd);
     }
 
+    @action
+    onMove = (pt: number[]) => {
+        const movLoc = this.getTransformedScreenPt(pt);
+        this._width = movLoc.x - this._start.x;
+        this._height = movLoc.y - this._start.y;
+    };
+
     @action
     onSelectMove = (e: PointerEvent) => {
         const movLoc = this.getTransformedScreenPt([e.clientX, e.clientY]);
@@ -248,9 +254,12 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         //e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged.  if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor.
     };
 
-    @action
     onSelectEnd = (e: PointerEvent) => {
         e.stopPropagation();
+        this.onEnd(e.clientX, e.clientY);
+    };
+    @action
+    onEnd = (x: number, y: number) => {
         const marquees = this.props.marqueeContainer.getElementsByClassName('marqueeAnnotator-dragBox');
         const marqueeStyle = (Array.from(marquees).lastElement() as HTMLDivElement)?.style;
         if (!this.isEmpty && marqueeStyle) {
@@ -266,9 +275,9 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
             copy.style.height = parseInt(marqueeStyle.height.toString().replace('px', '')) / scale + 'px';
             (copy as any).marqueeing = true;
             MarqueeAnnotator.previewNewAnnotation(this.props.savedAnnotations(), this.props.annotationLayer, copy, this.props.getPageFromScroll?.(this.top) || 0);
-            AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
+            AnchorMenu.Instance.jumpTo(x, y);
         }
-        this.props.finishMarquee(this.isEmpty ? e.clientX : undefined, this.isEmpty ? e.clientY : undefined, e);
+        this.props.finishMarquee(this.isEmpty ? x : undefined, this.isEmpty ? y : undefined);
         this._width = this._height = 0;
     };
 
diff --git a/src/client/views/ObservableReactComponent.tsx b/src/client/views/ObservableReactComponent.tsx
index 9b2b00903..394d1eae2 100644
--- a/src/client/views/ObservableReactComponent.tsx
+++ b/src/client/views/ObservableReactComponent.tsx
@@ -14,8 +14,7 @@ export abstract class ObservableReactComponent<T> extends React.Component<T, {}>
         makeObservable(this);
     }
     componentDidUpdate(prevProps: Readonly<T>): void {
-        Object.keys(prevProps).forEach(action(pkey =>
-            (prevProps as any)[pkey] !== (this.props as any)[pkey] &&
+        Object.keys(prevProps).filter(pkey => (prevProps as any)[pkey] !== (this.props as any)[pkey]).forEach(action(pkey =>
                 ((this._props as any)[pkey] = (this.props as any)[pkey]))); // prettier-ignore
     }
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index cbd3ff358..195b1a04c 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1399,7 +1399,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
         const indent = 30;
-        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(this.selectedLink!);
+        const hasSelectedAnchor = this.selectedLink !== this.selectedDoc && LinkManager.Links(this.sourceAnchor).includes(this.selectedLink!);
 
         return (
             <>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 1adb0d9e5..7a15e67e4 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -25,6 +25,7 @@ import { FieldViewProps } from './nodes/FieldView';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PropertiesView } from './PropertiesView';
 import './StyleProvider.scss';
+import { CollectionSchemaView } from './collections/collectionSchema/CollectionSchemaView';
 
 export enum StyleProp {
     TreeViewIcon = 'treeView_Icon',
@@ -137,14 +138,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
         case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey';
         case StyleProp.Opacity:    return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 :  Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
         case StyleProp.FontSize:   return StrCast(doc?.[fieldKey + 'fontSize'],   StrCast(Doc.UserDoc().fontSize));
-        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
+        case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamilyq'], StrCast(Doc.UserDoc().fontFamily));
         case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'],  StrCast(Doc.UserDoc().fontWeight));
         case StyleProp.FillColor:  return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent')));
         case StyleProp.ShowCaption:return props?.hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption);
-        case StyleProp.TitleHeight:return (props?.ScreenToLocalTransform().Scale ?? 1) * NumCast(Doc.UserDoc().headerHeight,30);
+        case StyleProp.TitleHeight:return Math.min(4,(props?.DocumentView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30);
         case StyleProp.ShowTitle:
             return (
                 (doc &&
+                    !(props?.DocumentView?.().ComponentView instanceof CollectionSchemaView) &&
                     !props?.LayoutTemplateString &&
                     !doc.presentation_targetDoc &&
                     !props?.LayoutTemplateString?.includes(KeyValueBox.name) &&
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 5c47d4b9e..656f850b3 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -594,6 +594,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                     <StackedTimelineAnchor
                                         {...this._props}
                                         mark={d.anchor}
+                                        containerViewPath={this._props.containerViewPath}
                                         rangeClickScript={this.rangeClickScript}
                                         rangePlayScript={this.rangePlayScript}
                                         left={left - this._scroll}
@@ -701,6 +702,7 @@ interface StackedTimelineAnchorProps {
     layoutDoc: Doc;
     isDocumentActive?: () => boolean | undefined;
     ScreenToLocalTransform: () => Transform;
+    containerViewPath?: () => DocumentView[];
     _timeline: HTMLDivElement | null;
     focus: FocusFuncType;
     currentTimecode: () => number;
@@ -796,12 +798,15 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
         );
     };
 
+    resetTitle = () => {
+        this._props.mark[DocData].title = ComputedField.MakeFunction(
+            `["${this._props.endTag}"] ? "#" + formatToTime(this["${this._props.startTag}"]) + "-" + formatToTime(this["${this._props.endTag}"]) : "#" + formatToTime(this["${this._props.startTag}"]`
+        );
+    };
     // context menu
     contextMenuItems = () => {
         const resetTitle = {
-            script: ScriptField.MakeFunction(
-                `this.title = this["${this._props.endTag}"] ? "#" + formatToTime(this["${this._props.startTag}"]) + "-" + formatToTime(this["${this._props.endTag}"]) : "#" + formatToTime(this["${this._props.startTag}"])`
-            )!,
+            method: this.resetTitle,
             icon: 'folder-plus',
             label: 'Reset Title',
         };
@@ -826,7 +831,7 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                     ref={action((r: DocumentView | null) => (anchor.view = r))}
                     Document={mark}
                     TemplateDataDocument={undefined}
-                    containerViewPath={returnEmptyDoclist}
+                    containerViewPath={this._props.containerViewPath}
                     pointerEvents={this.noEvents ? returnNone : undefined}
                     styleProvider={this._props.styleProvider}
                     renderDepth={this._props.renderDepth + 1}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index b8c0967c1..6484658e0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -388,7 +388,7 @@ function normalizeResults(
     minWidth: number,
     extras: ViewDefBounds[]
 ): ViewDefResult[] {
-    const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height } as ViewDefBounds));
+    const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
     const docEles = Array.from(docMap.entries()).map(ele => ele[1]);
     const aggBounds = aggregateBounds(
         extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: 'doc', payload: '' })))).filter(e => e.zIndex !== -99),
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1fd453e96..5495735ea 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -565,7 +565,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     clusterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
         let styleProp = this._props.styleProvider?.(doc, props, property); // bcz: check 'props'  used to be renderDepth + 1
         if (doc && this.childDocList?.includes(doc))
-            switch (property) {
+            switch (property.split(':')[0]) {
                 case StyleProp.BackgroundColor:
                     const cluster = NumCast(doc?.layout_cluster);
                     if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
@@ -590,7 +590,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     trySelectCluster = (addToSel: boolean) => {
-        if (this._hitCluster !== -1) {
+        if (addToSel && this._hitCluster !== -1) {
             !addToSel && SelectionManager.DeselectAll();
             const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === this._hitCluster);
             this.selectDocuments(eles);
@@ -1288,15 +1288,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const rotation = Cast(_rotation,'number',
             !this.layoutDoc._rotation_jitter ? null
                 : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) );
+        const childProps = { ...this._props, styleProvider: this.clusterStyleProvider };
         return {
             x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
             y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
             z: Cast(z, 'number'),
             autoDim,
             rotation,
-            color: Cast(color, 'string') ? StrCast(color) : this._props.styleProvider?.(childDoc, this._props, StyleProp.Color),
-            backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.clusterStyleProvider(childDoc, this._props, StyleProp.BackgroundColor),
-            opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this._props.styleProvider?.(childDoc, this._props, StyleProp.Opacity),
+            color: Cast(color, 'string') ? StrCast(color) : this.clusterStyleProvider(childDoc, childProps, StyleProp.Color),
+            backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.clusterStyleProvider(childDoc, childProps, StyleProp.BackgroundColor),
+            opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.clusterStyleProvider?.(childDoc, childProps, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
             width: _width,
             height: _height,
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b913e05ad..cb593a993 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -107,7 +107,9 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         const cm = ContextMenu.Instance;
         const [x, y] = this.Transform.transformPoint(this._downX, this._downY);
         if (e.key === '?') {
-            cm.setDefaultItem('?', (str: string) => this._props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', data_useCors: true }), OpenWhere.addRight));
+            cm.setDefaultItem('?', (str: string) =>
+                this._props.addDocTab(Docs.Create.WebDocument(`https://wikipedia.org/wiki/${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: `wiki:${str}`, data_useCors: true }), OpenWhere.addRight)
+            );
             cm.displayMenu(this._downX, this._downY, undefined, true);
             e.stopPropagation();
         } else if (e.key === 'u' && this._props.ungroup) {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 67dc508d5..bda8afa41 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -213,7 +213,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro
 
     get url() {
         const field = Cast(this._props.Document[this._props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
-        const alts = DocListCast(this._props.Document[this._props.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
+        const alts = DocListCast(this._props.Document[this._props.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images
         const altpaths = alts
             .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
             .filter(url => url)
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 3fdc9a488..d12639fc7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -87,8 +87,8 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
         });
     } else {
         Doc.SharingDoc().headingColor = undefined;
-        Doc.GetProto(Doc.SharingDoc()).headingColor = color;
-        Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date');
+        Doc.GetProto(Doc.SharingDoc()).headingColor = color === 'transparent' ? undefined : color;
+        Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'title');
     }
 });
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2800ea200..0d0a7c623 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { OmitKeys, numberRange } from '../../../Utils';
@@ -17,6 +17,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import './CollectionFreeFormDocumentView.scss';
 import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import { FieldViewProps } from './FieldView';
+import { TransitionTimer } from '../../../fields/DocSymbols';
 
 /// Ugh, typescript has no run-time way of iterating through the keys of an interface.  so we need
 /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface
@@ -91,6 +92,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     @observable AutoDim = this.props.autoDim;
     @observable Transition = this.props.transition;
 
+    componentDidMount(): void {
+        if (this.props.transition && !this.Document[TransitionTimer]) {
+            const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]);
+            this.Document[TransitionTimer] = setTimeout(
+                action(() => (this.Document[TransitionTimer] = this.Transition = undefined)),
+                num
+            );
+        }
+    }
+
     componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) {
         super.componentDidUpdate(prevProps);
         this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])));
@@ -98,14 +109,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
     // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
-    DataTransition = ()  => this._props.transition; // prettier-ignore
+    DataTransition = ()    => this.Transition || StrCast(this.Document.dataTransition); // prettier-ignore
     RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
     PanelWidth = ()        => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore
     PanelHeight = ()       => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore
 
     styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
         if (doc === this.layoutDoc) {
-            switch (property) {
+            switch (property.split(':')[0]) {
                 case StyleProp.Opacity:         return this.Opacity; // only change the opacity for this specific document, not its children
                 case StyleProp.BackgroundColor: return this.BackgroundColor;
                 case StyleProp.Color:           return this.Color;
@@ -224,7 +235,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     //  undefined - this is not activated by a group
     isGroupActive = () => {
         if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined;
-        const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+        const backColor = this.BackgroundColor;
+        const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent');
         return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined;
     };
     render() {
@@ -237,7 +249,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     width: this.PanelWidth(),
                     height: this.PanelHeight(),
                     transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`,
-                    transition: this.Transition || StrCast(this.Document.dataTransition),
+                    transition: this.DataTransition(),
                     zIndex: this.ZIndex,
                     display: this.Width ? undefined : 'none',
                 }}>
@@ -251,6 +263,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                         styleProvider={this.styleProvider}
                         ScreenToLocalTransform={this.screenToLocalTransform}
                         isGroupActive={this.isGroupActive}
+                        PanelWidth={this.PanelWidth}
+                        PanelHeight={this.PanelHeight}
                     />
                 )}
             </div>
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 5a326ecb0..e729e2fa2 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -7,7 +7,7 @@ import { OmitKeys, Without, emptyPath } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { AclPrivate, DocData } from '../../../fields/DocSymbols';
 import { ScriptField } from '../../../fields/ScriptField';
-import { Cast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { InkingStroke } from '../InkingStroke';
 import { ObservableReactComponent } from '../ObservableReactComponent';
@@ -128,7 +128,9 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
         if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString;
         if (!this.layoutDoc) return '<p>awaiting layout</p>';
         if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString());
-        const layout = Cast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string');
+        const tempLayout = DocCast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')]);
+        const layoutDoc = tempLayout ?? this.layoutDoc;
+        const layout = Cast(layoutDoc[layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(layoutDoc.layout_fieldKey, 'layout')], 'string');
         if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString();
         if (typeof layout === 'string') return layout;
         return '<p>Loading layout</p>';
@@ -154,6 +156,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
             'LayoutTemplate',
             'layoutFieldKey',
             'dontCenter',
+            'DataTransition',
             'contextMenuItems',
             //'onClick', // don't need to omit this since it will be set
             'onDoubleClickScript',
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1044d6609..3154ee434 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { Howl } from 'howler';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
@@ -104,12 +104,11 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
     dragWhenActive?: boolean;
     dontHideOnDrag?: boolean;
-    suppressSetHeight?: boolean;
     onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
     DataTransition?: () => string | undefined;
     NativeWidth?: () => number;
     NativeHeight?: () => number;
-    contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[];
+    contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[];
     dragConfig?: (data: DragManager.DocumentDragData) => void;
     dragStarting?: () => void;
     dragEnding?: () => void;
@@ -560,7 +559,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
             cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' })
         );
-        this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp }));
+        this._props
+            .contextMenuItems?.()
+            .forEach(
+                item =>
+                    item.label &&
+                    cm.addItem({ description: item.label, event: () => (item.method ? item.method() : item.script?.script.run({ this: this.Document, documentView: this, scriptContext: this._props.scriptContext })), icon: item.icon as IconProp })
+            );
 
         if (!this.Document.isFolder) {
             const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null);
@@ -597,7 +602,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 if (!this.Document.annotationOn) {
                     onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
                     !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
-                    cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
+                    !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
                 } else if (LinkManager.Links(this.Document).length) {
                     onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' });
                     onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
@@ -810,8 +815,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
     /**
      * displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by
-     * setting layout_showTitle using the format:   field1[;field2[...][:hover]]
-     * from the UI, this is done by clicking the title field and prefixin the format with '#'.  eg.,  #field1[;field2;...][:hover]
+     * setting layout_showTitle using the format:   field1[:hover]
      **/
     @computed get titleView() {
         const showTitle = this.layout_showTitle?.split(':')[0];
@@ -851,21 +855,26 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         contents={
                             showTitle
                                 .split(';')
-                                .map(field => Field.toString(targetDoc[field.trim()] as Field))
+                                .map(field => Field.toJavascriptString(this.Document[field] as Field))
                                 .join(' \\ ') || '-unset-'
                         }
                         display="block"
                         oneLine={true}
                         fontSize={(this.titleHeight / 15) * 10}
-                        GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.Document, showTitle.split(';')[0]))}
+                        GetValue={() =>
+                            showTitle
+                                .split(';')
+                                .map(field => Field.toKeyValueString(this.Document, field))
+                                .join('\\')
+                        }
                         SetValue={undoBatch((input: string) => {
-                            if (input?.startsWith('#')) {
+                            if (input?.startsWith('$')) {
                                 if (this.layoutDoc.layout_showTitle) {
-                                    this.layoutDoc._layout_showTitle = input?.substring(1);
+                                    this.layoutDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
                                 } else if (!this._props.layout_showTitle) {
-                                    Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date';
+                                    Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title';
                                 }
-                            } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') {
+                            } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') {
                                 KeyValueBox.SetField(targetDoc, showTitle, input);
                             }
                             return true;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 5b47dd91d..771856788 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -92,6 +92,7 @@ export interface FieldViewSharedProps {
     waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
     defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
     pointerEvents?: () => Opt<string>;
+    suppressSetHeight?: boolean;
 }
 
 /**
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 251235b93..86e8ed60a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -9,7 +9,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -29,6 +29,12 @@ import { OpenWhere } from './DocumentView';
 import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import { PinProps, PresBox } from './trails';
+import { Colors } from 'browndash-components';
+import { listSpec } from '../../../fields/Schema';
+import { List } from '../../../fields/List';
+import { url } from 'inspector';
+import { OverlayView } from '../OverlayView';
+import { Networking } from '../../Network';
 
 @observer
 export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -134,7 +140,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) {
                 added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => {
                     this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
-                    return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
+                    return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop);
                 }, true);
             } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
                 const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
@@ -259,7 +265,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         const lower = url.href.toLowerCase();
         if (url.protocol === 'data') return url.href;
         if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href);
-        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
+        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`;
 
         const ext = extname(url.href);
         return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
@@ -297,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
                     ref={this._overlayIconRef}
                     onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))}
                     style={{
-                        display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none',
+                        display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none',
                         width: 'min(10%, 25px)',
                         height: 'min(10%, 25px)',
                         background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
@@ -311,13 +317,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
 
     @computed get paths() {
         const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
-        const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
-        const altpaths = alts
-            .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
-            .filter(url => url)
-            .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+        const alts = this.dataDoc[this.fieldKey + '_alternates'] as any as List<Doc>; // retrieve alternate documents that may be rendered as alternate images
+        const defaultUrl = new URL(Utils.prepend('/assets/unknown-file-icon-hi.png'));
+        const altpaths =
+            alts
+                ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl))
+                .filter(url => url)
+                .map(url => this.choosePath(url)) ?? []; // acc  ess the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
-        return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')];
+        return paths.length ? paths : [defaultUrl.href];
     }
 
     @observable _error = '';
@@ -326,7 +334,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     @computed get content() {
         TraceMobx();
 
-        const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor));
+        const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) ?? Colors.WHITE);
         const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1;
         const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
         const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
@@ -370,6 +378,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     }
     screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale);
     marqueeDown = (e: React.PointerEvent) => {
+        if (!this.dataDoc[this.fieldKey]) return this.chooseImage();
         if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
             setupMoveUpEvents(
                 this,
@@ -468,4 +477,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
             </div>
         );
     }
+
+    public chooseImage = () => {
+        const input = document.createElement('input');
+        input.type = 'file';
+        input.multiple = true;
+        input.accept = 'image/*';
+        input.onchange = async _e => {
+            const file = input.files?.[0];
+            if (file) {
+                const disposer = OverlayView.ShowSpinner();
+                const [{ result }] = await Networking.UploadFilesToServer({ file });
+                if (result instanceof Error) {
+                    alert('Error uploading files - possibly due to unsupported file types');
+                } else {
+                    this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+                    !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+                }
+                disposer();
+            } else {
+                console.log('No file selected');
+            }
+        };
+        input.click();
+    };
 }
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 5394c7dbb..78e4435ce 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -113,7 +113,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
                     if (key) target[key] = script.originalScript;
                     return false;
                 }
-                field === undefined && (field = res.result);
+                field === undefined && (field = res.result instanceof Array ? new List<any>(res.result) : res.result);
             }
         }
         if (!key) return false;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index be20b5934..74e78c671 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -41,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @computed get Title() {
-        return Field.toString(this.dataDoc[this.fieldKey] as Field);
+        return Field.toString(this.dataDoc[this.fieldKey] as Field) || StrCast(this.Document.title);
     }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 6e4d0e92a..0809e2ad6 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -2,12 +2,13 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import Xarrow from 'react-xarrows';
+import { FieldResult } from '../../../fields/Doc';
 import { DocCss, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
 import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils';
 import { DocumentManager } from '../../util/DocumentManager';
-import { LinkManager } from '../../util/LinkManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { EditableView } from '../EditableView';
@@ -22,8 +23,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string = 'link') {
         return FieldView.LayoutString(LinkBox, fieldKey);
     }
-    disposer: IReactionDisposer | undefined;
-    @observable _forceAnimate = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor
+    _disposer: IReactionDisposer | undefined;
+    @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor
     @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor
 
     constructor(props: FieldViewProps) {
@@ -39,11 +40,11 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
     };
     componentWillUnmount() {
-        this.disposer?.();
+        this._disposer?.();
     }
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this.disposer = reaction(
+        this._disposer = reaction(
             () => ({ drag: SnappingManager.IsDragging }),
             ({ drag }) => {
                 !LightboxView.Contains(this.DocumentView?.()) &&
@@ -64,12 +65,13 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             }
                         })
                     );
-            },
-            { fireImmediately: true }
+            }
         );
     }
 
     render() {
+        TraceMobx();
+
         if (this._hide) return null;
         const a = this.anchor1;
         const b = this.anchor2;
@@ -92,18 +94,34 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition
             const bt = b.getBounds?.transition; //    inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation)
 
+            var foundParent = false;
+            const getAnchor = (field: FieldResult): Element[] => {
+                const doc = DocCast(field);
+                const ele = document.getElementById(doc[Id]);
+                if (ele?.getBoundingClientRect().width) return [ele];
+                const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width);
+                const annoOn = DocCast(doc.annotationOn);
+                if (eles.length || !annoOn) return eles;
+                const pareles = getAnchor(annoOn);
+                foundParent = pareles.length ? true : false;
+                return pareles;
+            };
             //   if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>),
             //   then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
             //   otherwise, we just use the computed nearest point on the document boundary to the target Document
-            const targetAhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement();
-            const targetBhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement();
+            const targetAhyperlinks = getAnchor(this.dataDoc.link_anchor_1);
+            const targetBhyperlinks = getAnchor(this.dataDoc.link_anchor_2);
 
-            const aid = targetAhyperlink?.id || a.Document[Id];
-            const bid = targetBhyperlink?.id || b.Document[Id];
-            if (!document.getElementById(aid) || !document.getElementById(bid)) {
+            const container = this.DocumentView?.().containerViewPath?.().lastElement().ContentDiv;
+            const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id;
+            const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id;
+            if (!aid || !bid) {
                 setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01)));
                 return null;
             }
+            if (foundParent) {
+                setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 1)));
+            }
 
             if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation
             const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting);
@@ -192,6 +210,11 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </>
             );
         }
+
+        setTimeout(
+            action(() => (this._forceAnimate = this._forceAnimate + 1)),
+            2
+        );
         return (
             <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}>
                 <ComparisonBox
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b2ae7201c..0af0751bf 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -335,7 +335,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
         Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
         this._props.addDocument?.(imageSnapshot);
         const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
-        link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3);
+        // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3);  // do we need to set an end time? should default to +0.1
         setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
     };
 
@@ -373,7 +373,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
     getView = (doc: Doc, options: FocusViewOptions) => {
         if (this._stackedTimeline?.makeDocUnfiltered(doc)) {
             if (this.heightPercent === 100) {
-                this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
+                // do we want to always open up the timeline when followin a link? kind of clunky visually
+                //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
                 options.didMove = true;
             }
             return this._stackedTimeline.getView(doc, options);
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c9340edc0..f4d5eef05 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { htmlToText } from 'html-to-text';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as WebRequest from 'web-request';
@@ -340,8 +340,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     _textAnnotationCreator: (() => ObservableMap<number, HTMLDivElement[]>) | undefined;
     savedAnnotationsCreator: () => ObservableMap<number, HTMLDivElement[]> = () => this._textAnnotationCreator?.() || this._savedAnnotations;
 
+    @action
+    iframeMove = (e: PointerEvent) => {
+        const theclick = this.props
+            .ScreenToLocalTransform()
+            .inverse()
+            .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
+        this._marqueeref.current?.onMove(theclick);
+    };
     @action
     iframeUp = (e: PointerEvent) => {
+        this._iframe?.contentDocument?.removeEventListener('pointermove', this.iframeMove);
+        this.marqueeing = undefined;
         this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value
         this._textAnnotationCreator = undefined;
         this.DocumentView?.()?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here.
@@ -358,6 +368,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
                 GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
                 GPTPopup.Instance.addDoc = this.sidebarAddDocument;
             }
+        } else {
+            const theclick = this.props
+                .ScreenToLocalTransform()
+                .inverse()
+                .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
+            if (!this._marqueeref.current?.isEmpty) this._marqueeref.current?.onEnd(theclick[0], theclick[1]);
+            else {
+                if (!(e.target as any)?.tagName?.includes('INPUT')) this.finishMarquee(theclick[0], theclick[1]);
+                this._getAnchor = AnchorMenu.Instance?.GetAnchor;
+                this.marqueeing = undefined;
+            }
+
+            ContextMenu.Instance.closeMenu();
+            ContextMenu.Instance.setIgnoreEvents(false);
+            if (e?.button === 2 || e?.altKey) {
+                e?.preventDefault();
+                e?.stopPropagation();
+                setTimeout(() => {
+                    // if menu comes up right away, the down event can still be active causing a menu item to be selected
+                    this.specificContextMenu(undefined as any);
+                    this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]);
+                });
+            }
         }
     };
     @action
@@ -400,6 +433,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     };
     @action
     iframeDown = (e: PointerEvent) => {
+        this._textAnnotationCreator = undefined;
+        const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
+        if (sel?.empty)
+            sel.empty(); // Chrome
+        else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
+
         this._props.select(false);
         const theclick = this.props
             .ScreenToLocalTransform()
@@ -409,6 +448,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
         const word = getWordAtPoint(e.target, e.clientX, e.clientY);
         if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) {
             this.marqueeing = theclick;
+            this._marqueeref.current?.onInitiateSelection(this.marqueeing);
+            this._iframe?.contentDocument?.addEventListener('pointermove', this.iframeMove);
             e.preventDefault();
         }
     };
@@ -739,28 +780,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             this.marqueeing = undefined;
         }
     };
-    @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => {
+    @action finishMarquee = (x?: number, y?: number) => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
         this.marqueeing = undefined;
-        this._textAnnotationCreator = undefined;
-        const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
-        if (sel?.empty)
-            sel.empty(); // Chrome
-        else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
         this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document);
-        if (x !== undefined && y !== undefined) {
-            ContextMenu.Instance.closeMenu();
-            ContextMenu.Instance.setIgnoreEvents(false);
-            if (e?.button === 2 || e?.altKey) {
-                e?.preventDefault();
-                e?.stopPropagation();
-                setTimeout(() => {
-                    // if menu comes up right away, the down event can still be active causing a menu item to be selected
-                    this.specificContextMenu(undefined as any);
-                    this.DocumentView?.().onContextMenu(undefined, x, y);
-                });
-            }
-        }
     };
 
     @observable lighttext = false;
@@ -992,6 +1015,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
     }
     childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined);
     @computed get webpage() {
+        TraceMobx();
         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);
@@ -1071,6 +1095,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
             : 'none';
     annotationPointerEvents = () => (this._props.isContentActive() && (SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none');
     render() {
+        TraceMobx();
         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);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 793595694..eb7293054 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -450,16 +450,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     autoLink = () => {
         const newAutoLinks = new Set<Doc>();
-        const oldAutoLinks = LinkManager.Links(this.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords);
+        const oldAutoLinks = LinkManager.Links(this.Document).filter(
+            link => 
+                ((!Doc.isTemplateForField(this.Document) &&
+                    (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
+                    (!Doc.isTemplateForField(DocCast(link.link_anchor_2)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
+                 (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
+                link.link_relationship === LinkManager.AutoKeywords
+        ); // prettier-ignore
         if (this._editorView?.state.doc.textContent) {
-            const isNodeSel = this._editorView.state.selection instanceof NodeSelection;
             const f = this._editorView.state.selection.from;
+
             const t = this._editorView.state.selection.to;
             var tr = this._editorView.state.tr as any;
             const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
             tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
             Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
-            tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
+            tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
             this._editorView?.dispatch(tr);
         }
         oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink);
@@ -624,7 +631,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         docId: draggedDoc[Id],
                         float: 'unset',
                     });
-                    if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) {
+                    if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) {
                         added = dragData.removeDocument?.(draggedDoc) ? true : false;
                     } else {
                         added = true;
@@ -1814,7 +1821,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             };
             const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins);
             const scrollHeight = this.ProseRef && proseHeight;
-            if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) {
+            if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
                 // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
                 const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight);
 
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e34144fae..ae6da8fb0 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -703,7 +703,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (pinProps.pinData.temporal) {
                 pinDoc.config_clipStart = targetDoc._layout_currentTimecode;
                 const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1);
-                pinDoc.config_clipEnd = NumCast(targetDoc.clipEnd, duration);
+                pinDoc.config_clipEnd = NumCast(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration);
             }
         }
         if (pinProps?.pinViewport) {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 246828709..1bd49cf3f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -45,18 +45,23 @@ export namespace Field {
     export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string {
         const onDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, ''));
         const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
-        return !Field.IsField(field)
-            ? key.startsWith('_')
-                ? '='
-                : ''
-            : (onDelegate ? '=' : '') +
-                  (field instanceof ComputedField && showComputedValue
-                      ? field._lastComputedResult
-                      : field instanceof ComputedField
-                        ? `:=${field.script.originalScript.replace(/dashCallChat\(_setCacheResult_, this, `(.*)`\)/, '(($1))')}`
-                        : field instanceof ScriptField
-                          ? `$=${field.script.originalScript}`
-                          : Field.toScriptString(field));
+        const valFunc = (field: Field): string => {
+            const res =
+                field instanceof ComputedField && showComputedValue
+                    ? field._lastComputedResult
+                    : field instanceof ComputedField
+                      ? `:=${field.script.originalScript.replace(/dashCallChat\(_setCacheResult_, this, `(.*)`\)/, '(($1))')}`
+                      : field instanceof ScriptField
+                        ? `$=${field.script.originalScript}`
+                        : Field.toScriptString(field);
+            const resStr = (res + '').replace(/^`(.*)`$/, '$1');
+            return typeof field === 'string' && (+resStr).toString() !== resStr && !Array.from('+-*/.').some(k => Array.from(resStr).includes(k))
+                ? resStr
+                : (res + '') // adjust the key value string to be easier to enter: represent any initial list as an array with []
+                      .trim()
+                      .replace(/^new List\((.*)\)$/, '$1');
+        };
+        return !Field.IsField(field) ? (key.startsWith('_') ? '=' : '') : (onDelegate ? '=' : '') + valFunc(field);
     }
     export function toScriptString(field: Field) {
         switch (typeof field) {
diff --git a/src/fields/PresField.ts b/src/fields/PresField.ts
deleted file mode 100644
index f236a04fd..000000000
--- a/src/fields/PresField.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-//insert code here
-import { ObjectField } from "./ObjectField";
-
-export abstract class PresField extends ObjectField {
-
-}
\ No newline at end of file
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index f5801de73..d0a149506 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -36,11 +36,4 @@ export class RichTextField extends ObjectField {
     [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":[]}`,
-            ''
-        );
-    }
 }
-- 
cgit v1.2.3-70-g09d2


From bf6d1973cc81ba695afcca102c7229608faaa7e6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 27 Mar 2024 11:02:57 -0400
Subject: changed dashFieldViews to support Tab'ing between other
 dashFieldviews, changed deleting links to clear out the anchors so that
 linkBoxes will go away more easiliy.  changed funcitonPlot to plot the
 equations that are linked to it.  changed equations to link to functions.
 changed undo and other console logging to only happen when undo docked
 buttons are expanded (visible)

---
 src/client/DocServer.ts                            |  4 +-
 src/client/util/LinkManager.ts                     | 12 +++---
 src/client/util/UndoManager.ts                     | 21 ++++-----
 src/client/views/EditableView.tsx                  |  1 -
 src/client/views/PreviewCursor.tsx                 | 12 +++---
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/TemplateMenu.tsx                  |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/linking/LinkMenuGroup.tsx         |  2 +-
 src/client/views/nodes/EquationBox.tsx             |  6 ++-
 src/client/views/nodes/FunctionPlotBox.tsx         | 26 ++++++++---
 src/client/views/nodes/LinkBox.tsx                 | 10 +++--
 src/client/views/nodes/LinkDescriptionPopup.tsx    |  6 ++-
 src/client/views/nodes/ScriptingBox.tsx            |  4 +-
 .../views/nodes/formattedText/DashFieldView.tsx    | 50 ++++++++++++++++------
 .../views/nodes/formattedText/EquationView.tsx     |  7 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 ++--
 .../views/nodes/formattedText/RichTextRules.ts     |  6 +--
 src/debug/Viewer.tsx                               |  4 +-
 19 files changed, 117 insertions(+), 70 deletions(-)

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

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 6217cf04b..bd60a205c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -65,10 +65,10 @@ export namespace DocServer {
         cacheDocumentIds = newCacheUpdate;
 
         // print out cached docs
-        console.log('Set cached docs = ');
+        Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = ');
         const is_filtered = filtered.filter(doc => !Doc.IsSystem(doc));
         const strings = is_filtered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)'));
-        strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
+        Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
 
         rp.post(Utils.prepend('/setCacheDocumentIds'), {
             body: {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index cf16c4d6d..608e05fe9 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -152,10 +152,12 @@ export class LinkManager {
         }
     }
     public deleteLink(linkDoc: Doc) {
-        return Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc);
+        const ret = Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc);
+        linkDoc[DocData].link_anchor_1 = linkDoc[DocData].link_anchor_2 = undefined;
+        return ret;
     }
     public deleteAllLinksOnAnchor(anchor: Doc) {
-        LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
+        LinkManager.Instance.relatedLinker(anchor).forEach(LinkManager.Instance.deleteLink);
     }
 
     public getAllRelatedLinks(anchor: Doc) {
@@ -209,10 +211,10 @@ export class LinkManager {
         const a1 = DocCast(linkDoc.link_anchor_1);
         const a2 = DocCast(linkDoc.link_anchor_2);
         if (linkDoc.link_matchEmbeddings) {
-            return [a2, a2.annotationOn].includes(anchor) ? '2' : '1';
+            return [a2, a2?.annotationOn].includes(anchor) ? '2' : '1';
         }
-        if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1';
-        if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2';
+        if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1';
+        if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2';
         if (Doc.AreProtosEqual(anchor, linkDoc)) return '0';
 
         // const a1 = DocCast(linkDoc.link_anchor_1);
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 421855bf3..857ca852f 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,5 +1,5 @@
 import { observable, action, runInAction } from 'mobx';
-import { Field } from '../../fields/Doc';
+import { Doc, Field } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
 import { Without } from '../../Utils';
 
@@ -97,13 +97,14 @@ export namespace UndoManager {
 
     export function AddEvent(event: UndoEvent, value?: any): void {
         if (currentBatch && batchCounter.get() && !undoing) {
-            console.log(
-                '                         '.slice(0, batchCounter.get()) +
-                    'UndoEvent : ' +
-                    event.prop +
-                    ' = ' +
-                    (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value))
-            );
+            Doc.MyDockedBtns.linearView_IsOpen &&
+                console.log(
+                    '                         '.slice(0, batchCounter.get()) +
+                        'UndoEvent : ' +
+                        event.prop +
+                        ' = ' +
+                        (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value))
+                );
             currentBatch.push(event);
             tempEvents?.push(event);
         }
@@ -171,7 +172,7 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        Doc.MyDockedBtns.linearView_IsOpen && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
         runInAction(() => batchCounter.set(batchCounter.get() + 1));
         if (currentBatch === undefined) {
             currentBatch = [];
@@ -181,7 +182,7 @@ export namespace UndoManager {
 
     const EndBatch = action((batchName: string, cancel: boolean = false) => {
         runInAction(() => batchCounter.set(batchCounter.get() - 1));
-        console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+        Doc.MyDockedBtns.linearView_IsOpen && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
         if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 4508d00a7..44c8e4e1f 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -232,7 +232,6 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
                     onPointerDown: this.stopPropagation,
                     onClick: this.stopPropagation,
                     onPointerUp: this.stopPropagation,
-                    onKeyPress: this.stopPropagation,
                     value: this._props.autosuggestProps.value,
                     // @ts-ignore
                     onChange: this._props.autosuggestProps.onChange,
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index a94c18295..4b7771f27 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -18,7 +18,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         return PreviewCursor._instance;
     }
 
-    _onKeyPress?: (e: KeyboardEvent) => void;
+    _onKeyDown?: (e: KeyboardEvent) => void;
     _getTransform?: () => Transform;
     _addDocument?: (doc: Doc | Doc[]) => boolean;
     _addLiveTextDoc?: (doc: Doc) => void;
@@ -32,7 +32,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         makeObservable(this);
         PreviewCursor._instance = this;
         this._clickPoint = observable([0, 0]);
-        document.addEventListener('keydown', this.onKeyPress);
+        document.addEventListener('keydown', this.onKeyDown);
         document.addEventListener('paste', this.paste, true);
     }
 
@@ -120,7 +120,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     };
 
     @action
-    onKeyPress = (e: KeyboardEvent) => {
+    onKeyDown = (e: KeyboardEvent) => {
         // Mixing events between React and Native is finicky.
         //if not these keys, make a textbox if preview cursor is active!
         if (
@@ -146,7 +146,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         ) {
             if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) {
                 //  /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) {
-                this.Visible && this._onKeyPress?.(e);
+                this.Visible && this._onKeyDown?.(e);
                 ((!e.ctrlKey && !e.metaKey) || e.key !== 'v') && (this.Visible = false);
             }
         } else if (this.Visible) {
@@ -171,7 +171,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     public static Show(
         x: number,
         y: number,
-        onKeyPress: (e: KeyboardEvent) => void,
+        onKeyDown: (e: KeyboardEvent) => void,
         addLiveText: (doc: Doc) => void,
         getTransform: () => Transform,
         addDocument: undefined | ((doc: Doc | Doc[]) => boolean),
@@ -181,7 +181,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         const self = PreviewCursor.Instance;
         if (self) {
             self._clickPoint = [x, y];
-            self._onKeyPress = onKeyPress;
+            self._onKeyDown = onKeyDown;
             self._addLiveTextDoc = addLiveText;
             self._getTransform = getTransform;
             self._addDocument = addDocument || returnFalse;
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 195b1a04c..0de44b62b 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -715,7 +715,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     value={value}
                     style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
                     onChange={e => setter(e.target.value)}
-                    onKeyPress={e => e.stopPropagation()}
+                    onKeyDown={e => e.stopPropagation()}
                 />
                 <div className="inputBox-button">
                     <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e7269df98..5df0bea1a 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -99,7 +99,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 .forEach(template => templateMenu.push(<OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
         return (
             <ul className="template-list">
-                {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />}
+                {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyDown={this.onCustomKeypress} />}
                 {templateMenu}
                 <CollectionTreeView
                     Document={Doc.MyTemplates}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cb593a993..6b3a56b0b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -101,7 +101,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
 
     @action
-    onKeyPress = (e: KeyboardEvent) => {
+    onKeyDown = (e: KeyboardEvent) => {
         //make textbox and add it to this collection
         // tslint:disable-next-line:prefer-const
         const cm = ContextMenu.Instance;
@@ -311,7 +311,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             const effectiveAcl = GetEffectiveAcl(this._props.Document[DocData]);
             if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
                 PreviewCursor.Instance.Doc = doc;
-                PreviewCursor.Show(x, y, this.onKeyPress, this._props.addLiveTextDocument, this._props.getTransform, this._props.addDocument, this._props.nudge, this._props.slowLoadDocuments);
+                PreviewCursor.Show(x, y, this.onKeyDown, this._props.addLiveTextDocument, this._props.getTransform, this._props.addDocument, this._props.nudge, this._props.slowLoadDocuments);
             }
             this.clearSelection();
         }
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 028d3da53..60def5d45 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -58,7 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                       ? DocCast(linkDoc.link_anchor_2)
                       : DocCast(linkDoc.link_anchor_1)
                   : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) ||
-                    LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null));
+                    LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null)?.annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null));
             return !destDoc || !sourceDoc ? null : (
                 <LinkMenuItem
                     key={linkDoc[Id]}
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 50d4c7c78..8362db37a 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
 import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { LightboxView } from '../LightboxView';
@@ -80,7 +80,9 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 _height: 300,
                 backgroundColor: 'white',
             });
-            this._props.addDocument?.(graph);
+            const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' });
+            //this._props.addDocument?.(graph);
+            link && this._props.addDocument?.(link);
             e.stopPropagation();
         }
         if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 2e7a2120e..67445e552 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -7,12 +7,13 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
 import { PinProps, PresBox } from './trails';
+import { LinkManager } from '../../util/LinkManager';
 
 @observer
 export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -33,7 +34,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         reaction(
-            () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange],
+            () => [this.graphFuncs, this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange],
             () => this.createGraph()
         );
     }
@@ -45,11 +46,17 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         if (addAsAnnotation) this.addDocument(anchor);
         return anchor;
     };
+    @computed get graphFuncs() {
+        const links = LinkManager.Instance.getAllRelatedLinks(this.Document)
+            .map(d => LinkManager.getOppositeAnchor(d, this.Document))
+            .filter(d => d)
+            .map(d => d!);
+        return links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'));
+    }
     createGraph = (ele?: HTMLDivElement) => {
         this._plotEle = ele || this._plotEle;
         const width = this._props.PanelWidth();
         const height = this._props.PanelHeight();
-        const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'));
         try {
             this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
             this._plot = functionPlot({
@@ -59,7 +66,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
                 xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) },
                 yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
                 grid: true,
-                data: fns.map(fn => ({
+                data: this.graphFuncs.map(fn => ({
                     fn,
                     // derivative: { fn: "2 * x", updateOnMouseMove: true }
                 })),
@@ -72,7 +79,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     @undoBatch
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.docDragData?.droppedDocuments.length) {
-            const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc), true);
+            const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
+                ///const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc);
+                if (res) {
+                    const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' });
+                    link && this._props.addDocument?.(link);
+                }
+                return res;
+            }, true);
             !added && e.preventDefault();
             e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
             return added;
@@ -104,7 +118,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
                 {this.theGraph}
                 <div
                     style={{
-                        display: this._props.isSelected() ? 'none' : undefined,
+                        display: this._props.isContentActive() ? 'none' : undefined,
                         position: 'absolute',
                         width: '100%',
                         height: '100%',
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 0809e2ad6..8ebcefb35 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -23,7 +23,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string = 'link') {
         return FieldView.LayoutString(LinkBox, fieldKey);
     }
-    _disposer: IReactionDisposer | undefined;
+    _disposers: { [name: string]: IReactionDisposer } = {};
     @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor
     @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor
 
@@ -40,11 +40,15 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
     };
     componentWillUnmount() {
-        this._disposer?.();
+        Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._disposer = reaction(
+        this._disposers.deleting = reaction(
+            () => !this.anchor1 || !this.anchor2,
+            empty => empty && this._props.removeDocument?.(this.Document)
+        );
+        this._disposers.dragging = reaction(
             () => ({ drag: SnappingManager.IsDragging }),
             ({ drag }) => {
                 !LightboxView.Contains(this.DocumentView?.()) &&
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 1645d0813..2a96ce458 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -69,8 +69,10 @@ export class LinkDescriptionPopup extends React.Component<{}> {
                 }}>
                 <input
                     className="linkDescriptionPopup-input"
-                    onKeyDown={e => e.stopPropagation()}
-                    onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)}
+                    onKeyDown={e => {
+                        e.key === 'Enter' && this.onDismiss(true);
+                        e.stopPropagation();
+                    }}
                     value={this.description}
                     placeholder={this.description || '(Optional) Enter link description...'}
                     onChange={e => this.descriptionChanged(e)}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index d9d0dbe3e..8c65fd34e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -670,7 +670,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const definedParameters = !this.compileParams.length ? null : (
             <div className="scriptingBox-plist" style={{ width: '30%' }}>
                 {this.compileParams.map((parameter, i) => (
-                    <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+                    <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}>
                         <EditableView
                             display={'block'}
                             maxHeight={72}
@@ -749,7 +749,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 {!this.compileParams.length || !this.paramsNames ? null : (
                     <div className="scriptingBox-plist">
                         {this.paramsNames.map((parameter: string, i: number) => (
-                            <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+                            <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}>
                                 <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}>
                                     <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div>
                                     {this.paramsTypes[i] === 'boolean' ? this.renderEnum(parameter, [true, false]) : null}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 5b03e2236..8802a032f 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
@@ -22,15 +22,20 @@ import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
 import { DocData } from '../../../../fields/DocSymbols';
+import { NodeSelection, TextSelection } from 'prosemirror-state';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
     root: any;
     node: any;
     tbox: FormattedTextBox;
+    @observable expanded = false;
+    Expanded = () => this.expanded;
 
     unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        makeObservable(this);
+        const self = this;
         this.node = node;
         this.tbox = tbox;
         this.dom = document.createElement('div');
@@ -38,11 +43,26 @@ export class DashFieldView {
         this.dom.style.height = node.attrs.height;
         this.dom.style.position = 'relative';
         this.dom.style.display = 'inline-block';
-        this.dom.onkeypress = function (e: any) {
+        const tBox = this.tbox;
+        this.dom.onkeypress = function (e: KeyboardEvent) {
             e.stopPropagation();
         };
-        this.dom.onkeydown = function (e: any) {
+        this.dom.onkeydown = function (e: KeyboardEvent) {
             e.stopPropagation();
+            if (e.key === 'Tab') {
+                e.preventDefault();
+                const editor = tbox.EditorView;
+                if (editor) {
+                    const state = editor.state;
+                    for (var i = state.selection.to; i < state.doc.content.size; i++) {
+                        if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) {
+                            editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i))));
+                            return;
+                        }
+                    }
+                    tBox.setFocus(state.selection.to + 1);
+                }
+            }
         };
         this.dom.onkeyup = function (e: any) {
             e.stopPropagation();
@@ -51,6 +71,8 @@ export class DashFieldView {
             e.stopPropagation();
         };
 
+        this.expanded = node.attrs.expanded;
+
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
             <DashFieldViewInternal
@@ -63,7 +85,7 @@ export class DashFieldView {
                 height={node.attrs.height}
                 hideKey={node.attrs.hideKey}
                 editable={node.attrs.editable}
-                expanded={node.attrs.expanded}
+                expanded={this.Expanded}
                 dataDoc={node.attrs.dataDoc}
                 tbox={tbox}
             />
@@ -77,9 +99,11 @@ export class DashFieldView {
         });
     }
     deselectNode() {
+        runInAction(() => (this.expanded = false));
         this.dom.classList.remove('ProseMirror-selectednode');
     }
     selectNode() {
+        setTimeout(() => runInAction(() => (this.expanded = true)), 100);
         this.dom.classList.add('ProseMirror-selectednode');
     }
 }
@@ -92,7 +116,7 @@ interface IDashFieldViewInternal {
     width: number;
     height: number;
     editable: boolean;
-    expanded: boolean;
+    expanded: () => boolean;
     dataDoc: boolean;
     node: any;
     getPos: any;
@@ -106,7 +130,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     _fieldKey: string;
     _fieldRef = React.createRef<HTMLDivElement>();
     @observable _dashDoc: Doc | undefined = undefined;
-    @observable _expanded = this._props.expanded;
+    @observable _expanded = this._props.expanded();
 
     constructor(props: IDashFieldViewInternal) {
         super(props);
@@ -132,7 +156,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     componentWillUnmount() {
         this._reactionDisposer?.();
     }
-    isRowActive = () => this._expanded && this._props.editable;
+    isRowActive = () => (this._props.expanded() || this._expanded) && this._props.editable;
 
     finishEdit = action(() => {
         if (this._expanded) {
@@ -155,7 +179,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
                     maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
-                    columnWidth={this._expanded ? this.columnWidth : returnZero}
+                    columnWidth={this._expanded || this._props.expanded() ? this.columnWidth : returnZero}
                     selectedCell={this.selectedCell}
                     fieldKey={this._fieldKey}
                     rowHeight={returnZero}
@@ -164,7 +188,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     getFinfo={emptyFunction}
                     setColumnValues={returnFalse}
                     allowCRs={true}
-                    oneLine={!this._expanded}
+                    oneLine={!this._expanded && !this._props.expanded()}
                     finishEdit={this.finishEdit}
                     transform={Transform.Identity}
                     menuTarget={null}
@@ -217,7 +241,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     };
 
     @computed get values() {
-        if (this._props.expanded) return [];
+        if (this._props.expanded()) return [];
         const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
 
         return vals.strings.map(facet => ({ value: facet, label: facet }));
@@ -239,13 +263,13 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     </span>
                 )}
                 {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
-                {!this.values.length ? null : (
-                    <select className="dashFieldView-select" onChange={this.selectVal}>
+                {/* {!this.values.length ? null : (
+                    <select className="dashFieldView-select" tabIndex={-1} onChange={this.selectVal}>
                         {this.values.map(val => (
                             <option value={val.value}>{val.label}</option>
                         ))}
                     </select>
-                )}
+                )} */}
             </div>
         );
     }
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index b786c5ffb..b90653acc 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -8,6 +8,7 @@ import { StrCast } from '../../../../fields/Types';
 import './DashFieldView.scss';
 import EquationEditor from './EquationEditor';
 import { FormattedTextBox } from './FormattedTextBox';
+import { DocData } from '../../../../fields/DocSymbols';
 
 export class EquationView {
     dom: HTMLDivElement; // container for label and value
@@ -88,7 +89,6 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
                     }
                     e.stopPropagation();
                 }}
-                onKeyPress={e => e.stopPropagation()}
                 style={{
                     position: 'relative',
                     display: 'inline-block',
@@ -96,12 +96,11 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
                     height: this.props.height,
                     background: 'white',
                     borderRadius: '10%',
-                    bottom: 3,
                 }}>
                 <EquationEditor
                     ref={this._ref}
-                    value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')}
-                    onChange={(str: any) => (this._textBoxDoc[this._fieldKey] = str)}
+                    value={StrCast(this._textBoxDoc[DocData][this._fieldKey])}
+                    onChange={(str: any) => (this._textBoxDoc[DocData][this._fieldKey] = str)}
                     autoCommands="pi theta sqrt sum prod alpha beta gamma rho"
                     autoOperatorNames="sin cos tan"
                     spaceBehavesLikeTab={true}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index eb7293054..70b3b52fd 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1616,10 +1616,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.stopPropagation();
         }
     };
-    setFocus = () => {
-        const pos = this._editorView?.state.selection.$from.pos || 1;
-        (this.ProseRef?.children?.[0] as any).focus();
-        setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+    setFocus = (ipos?: number) => {
+        const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1);
+        setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100);
+        setTimeout(() => (this.ProseRef?.children?.[0] as any).focus(), 200);
     };
     @action
     onFocused = (e: React.FocusEvent): void => {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index adc031636..c3eaf4dd2 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -385,10 +385,10 @@ export class RichTextRules {
             }),
 
             // create an inline equation node
-            // eq:<equation>>
-            new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+            // %eq
+            new InputRule(new RegExp(/%eq/), (state, match, start, end) => {
                 const fieldKey = 'math' + Utils.GenerateGuid();
-                this.TextBox.dataDoc[fieldKey] = match[1];
+                this.TextBox.dataDoc[fieldKey] = 'y=';
                 const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
                 return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
             }),
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index f46adef77..ceff01fc2 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -152,7 +152,7 @@ class Viewer extends React.Component {
     };
 
     @action
-    onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+    onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
         if (e.key === 'Enter') {
             DocServer.GetRefField(this.idToAdd).then(
                 action((field: any) => {
@@ -168,7 +168,7 @@ class Viewer extends React.Component {
     render() {
         return (
             <>
-                <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyPress} />
+                <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyDown} />
                 <div>
                     {this.fields.map((field, index) => (
                         <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>
-- 
cgit v1.2.3-70-g09d2


From d38f8a80cff0e7e6140fd3ff815210077718746d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 29 Mar 2024 17:44:26 -0400
Subject: fixed capture offset to target in link following.  fixed links to
 appear in lightbox and to not disappear when dragged.

---
 src/client/util/LinkFollower.ts         | 24 ++++++++--------
 src/client/views/LightboxView.tsx       |  7 ++++-
 src/client/views/PropertiesView.tsx     |  3 +-
 src/client/views/nodes/DocumentView.tsx | 18 +++++++++---
 src/client/views/nodes/LinkBox.tsx      | 49 ++++++++++++++++-----------------
 5 files changed, 56 insertions(+), 45 deletions(-)

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

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 20261859c..6c0bf3242 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -110,18 +110,18 @@ export class LinkFollower {
                         movedTarget = true;
                     }
                     Doc.SetContainer(target, sourceDocParent);
-                    const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
-                    if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
-                        target.x = moveTo[0];
-                        movedTarget = true;
-                    }
-                    if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
-                        target.y = moveTo[1];
-                        movedTarget = true;
-                    }
-                    if (movedTarget) setTimeout(doFollow);
-                    else doFollow(true);
-                } else doFollow(true);
+                }
+                const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
+                if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
+                    target.x = moveTo[0];
+                    movedTarget = true;
+                }
+                if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
+                    target.y = moveTo[1];
+                    movedTarget = true;
+                }
+                if (movedTarget) setTimeout(doFollow);
+                else doFollow(true);
             } else {
                 allFinished();
             }
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index be7c22c9f..79700d8ab 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -35,7 +35,12 @@ const savedKeys = ['freeform_panX', 'freeform_panY', 'freeform_scale', 'layout_s
 type LightboxSavedState = { [key: string]: FieldResult; }; // prettier-ignore
 @observer
 export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
-    public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView&&  (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore
+    /**
+     * Determines whether a DocumentView is descendant of the lightbox view
+     * @param view
+     * @returns true if a DocumentView is descendant of the lightbox view
+     */
+    public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore
     public static get LightboxDoc() { return LightboxView.Instance?._doc; } // prettier-ignore
     static Instance: LightboxView;
     private _path: {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 0de44b62b..dc814bb16 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -71,7 +71,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get selectedLink() {
-        return this.selectedDocumentView?.ComponentView instanceof LinkBox ? this.selectedDocumentView.Document : LinkManager.Instance.currentLink;
+        return LinkManager.Instance.currentLink;
     }
 
     @computed get selectedLayoutDoc() {
@@ -1615,7 +1615,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     render() {
         const isNovice = Doc.noviceMode;
-        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(this.selectedLink!);
         if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this._props.width }}>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3e8d672d6..58820a498 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -921,7 +921,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             : this.docContents ?? (
                   <div
                       className="documentView-node"
-                      id={this.Document.type !== DocumentType.LINK ? this.Document[Id] : undefined}
+                      id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined}
                       style={{
                           ...style,
                           background: this.backgroundBoxColor,
@@ -1065,8 +1065,18 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _viewTimer: NodeJS.Timeout | undefined;
     private _animEffectTimer: NodeJS.Timeout | undefined;
-    public Guid = Utils.GenerateGuid(); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations.
-
+    /**
+     * This is used to create an id for tracking a Doc.  Since the Doc can be in a regular view and in the lightbox at
+     * the same time, this creates a different version of the id depending on whether the search scope will be in the lightbox or not.
+     * @param inLightbox is the id scoped to the lightbox
+     * @param id the id
+     * @returns
+     */
+    public static UniquifyId(inLightbox: boolean | undefined, id: string) {
+        return (inLightbox ? 'lightbox-' : '') + id;
+    }
+    public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations.
+    public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]);
     @computed public static get exploreMode() {
         return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
     }
@@ -1422,7 +1432,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined;
 
         return (
-            <div id={this.Guid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
+            <div id={this.ViewGuid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
                 {!this.Document || !this._props.PanelWidth() ? null : (
                     <div
                         className="contentFittingDocumentView-previewDoc"
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index b5cb7a9ae..7ad250714 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -17,6 +17,7 @@ import { StyleProp } from '../StyleProvider';
 import { ComparisonBox } from './ComparisonBox';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkBox.scss';
+import { DocumentView } from './DocumentView';
 
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -47,31 +48,27 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         this._disposers.deleting = reaction(
-            () => !this.anchor1 || !this.anchor2,
-            empty => empty && (this._hackToSeeIfDeleted = setTimeout(() => this._props.removeDocument?.(this.Document), 1000))
+            () => (!this.anchor1 || !this.anchor2) && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())),
+            empty => empty && ((this._hackToSeeIfDeleted = setTimeout(() => 
+                (!this.anchor1 || !this.anchor2) && this._props.removeDocument?.(this.Document)
+            )), 1000) // prettier-ignore
         );
         this._disposers.dragging = reaction(
-            () => ({ drag: SnappingManager.IsDragging }),
-            ({ drag }) => {
-                !LightboxView.Contains(this.DocumentView?.()) &&
-                    setTimeout(
-                        // need to wait for drag manager to set 'hidden' flag on dragged DOM elements
-                        action(() => {
-                            const a = this.anchor1,
-                                b = this.anchor2;
-                            let a1 = a && document.getElementById(a.Guid);
-                            let a2 = b && document.getElementById(b.Guid);
-                            // test whether the anchors themselves are hidden,...
-                            if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true;
-                            else {
-                                // .. or whether and of their DOM parents are hidden
-                                for (; a1 && !a1.hidden; a1 = a1.parentElement);
-                                for (; a2 && !a2.hidden; a2 = a2.parentElement);
-                                this._hide = a1 || a2 ? true : false;
-                            }
-                        })
-                    );
-            }
+            () => SnappingManager.IsDragging,
+            () => setTimeout( action(() => {// need to wait for drag manager to set 'hidden' flag on dragged DOM elements
+                const a = this.anchor1,
+                      b = this.anchor2;
+                let a1 = a && document.getElementById(a.ViewGuid);
+                let a2 = b && document.getElementById(b.ViewGuid);
+                // test whether the anchors themselves are hidden,...
+                if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true;
+                else {
+                    // .. or whether any of their DOM parents are hidden
+                    for (; a1 && !a1.hidden; a1 = a1.parentElement);
+                    for (; a2 && !a2.hidden; a2 = a2.parentElement);
+                    this._hide = a1 || a2 ? true : false;
+                }
+            })) // prettier-ignore
         );
     }
 
@@ -84,7 +81,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this._forceAnimate;
         const docView = this._props.docViewPath().lastElement();
 
-        if (a && b && !LightboxView.Contains(docView)) {
+        if (a && b) {
             // text selection bounds are not directly observable, so we have to
             // force an update when anything that could affect them changes (text edits causing reflow, scrolling)
             a.Document[a.LayoutFieldKey];
@@ -104,7 +101,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const getAnchor = (field: FieldResult): Element[] => {
                 const docField = DocCast(field);
                 const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
-                const ele = document.getElementById(doc[Id]);
+                const ele = document.getElementById(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id]));
                 if (ele?.className === 'linkBox-label') foundParent = true;
                 if (ele?.getBoundingClientRect().width) return [ele];
                 const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width);
@@ -178,7 +175,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         color={color}
                         labels={
                             <div
-                                id={this.Document[Id]}
+                                id={this.DocumentView?.().DocUniqueId}
                                 className={'linkBox-label'}
                                 style={{
                                     borderRadius: '8px',
-- 
cgit v1.2.3-70-g09d2


From ccbe6cafa422616ec4ffe5c2d1caed7597719007 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 30 Mar 2024 10:14:19 -0400
Subject: cleanup of import orderings.

---
 src/client/util/CaptureManager.tsx                 |   2 +-
 src/client/views/DocumentDecorations.tsx           |   3 +-
 src/client/views/GlobalKeyHandler.ts               |   8 +-
 src/client/views/LightboxView.tsx                  |   2 +-
 src/client/views/PropertiesView.tsx                |   3 +-
 src/client/views/collections/CollectionMenu.tsx    |  12 +-
 .../views/collections/CollectionNoteTakingView.tsx |   7 +-
 src/client/views/collections/TabDocView.tsx        |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   4 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  14 +-
 .../views/newlightbox/Header/LightboxHeader.tsx    | 108 ++++----
 .../RecommendationList/RecommendationList.tsx      | 295 +++++++++++----------
 src/client/views/nodes/DocumentView.tsx            |   3 +-
 src/client/views/nodes/EquationBox.tsx             |   2 +-
 src/client/views/nodes/LinkBox.tsx                 |   2 +-
 src/client/views/nodes/WebBox.tsx                  |   4 +-
 src/client/views/nodes/trails/PresBox.tsx          |   4 +-
 src/client/views/topbar/TopBar.tsx                 |   6 +-
 18 files changed, 253 insertions(+), 228 deletions(-)

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

diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 2e13aff2f..8451357ef 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -2,9 +2,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { addStyleSheet } from '../../Utils';
 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';
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4d9b93896..6698cd5bc 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -10,7 +10,6 @@ import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
 import { InkField } from '../../fields/InkField';
-import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl } from '../../fields/util';
@@ -33,8 +32,8 @@ import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { Colors } from './global/globalEnums';
 import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 667d8dbb0..2f64ea28c 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -13,18 +13,18 @@ import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { UndoManager } from '../util/UndoManager';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
-import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
 import { ContextMenu } from './ContextMenu';
 import { DocumentDecorations } from './DocumentDecorations';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
 import { MainView } from './MainView';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { OpenWhereMod } from './nodes/DocumentView';
-import { AnchorMenu } from './pdf/AnchorMenu';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { AnchorMenu } from './pdf/AnchorMenu';
 
 const modifiers = ['control', 'meta', 'shift', 'alt'];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 3d77ee901..ef4b5b4ca 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -19,9 +19,9 @@ import { Transform } from '../util/Transform';
 import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
-import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 import { TabDocView } from './collections/TabDocView';
 import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index dc814bb16..ae29382c1 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -10,7 +10,7 @@ import * as React from 'react';
 import { ColorResult, SketchPicker } from 'react-color';
 import * as Icons from 'react-icons/bs'; //{BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
 import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils';
-import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { Doc, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
 import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
@@ -41,7 +41,6 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
-import { LinkBox } from './nodes/LinkBox';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 interface PropertiesViewProps {
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 8729ef549..4f25f69ef 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -1,15 +1,16 @@
-import * as React from 'react';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { Toggle, ToggleType, Type } from 'browndash-components';
-import { action, computed, Lambda, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { Lambda, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { RichTextField } from '../../../fields/RichTextField';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
@@ -19,11 +20,10 @@ import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu } from '../AntimodeMenu';
 import { EditableView } from '../EditableView';
 import { MainView } from '../MainView';
-import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
 import { DefaultStyleProvider } from '../StyleProvider';
-import { CollectionLinearView } from './collectionLinear';
+import { DocumentView, DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
 import './CollectionMenu.scss';
-import { DocData } from '../../../fields/DocSymbols';
+import { CollectionLinearView } from './collectionLinear';
 
 interface CollectionMenuProps {
     panelHeight: () => number;
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 6318620e0..5b91216cb 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, IReactionDisposer, makeObservable, observable, observe, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, Field, Opt } from '../../../fields/Doc';
@@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DivHeight, emptyFunction, returnFalse, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { DivHeight, emptyFunction, returnZero, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -19,14 +19,13 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { LightboxView } from '../LightboxView';
 import { DocumentView } from '../nodes/DocumentView';
-import { FocusViewOptions, FieldViewProps } from '../nodes/FieldView';
+import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
 import './CollectionNoteTakingView.scss';
 import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
 import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
 import { CollectionSubView } from './CollectionSubView';
-import { JsxElement } from 'typescript';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 /**
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 699ec3b95..3ff58c326 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -30,7 +30,7 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
 import { Colors } from '../global/globalEnums';
 import { DocumentView, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from '../nodes/DocumentView';
-import { FocusViewOptions, FieldViewProps } from '../nodes/FieldView';
+import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
 import { KeyValueBox } from '../nodes/KeyValueBox';
 import { DashFieldView } from '../nodes/formattedText/DashFieldView';
 import { PinProps, PresBox, PresMovement } from '../nodes/trails';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1b562939b..d435173f3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
@@ -17,7 +17,7 @@ import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, numberValue, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 5f4948808..df023b00f 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,33 +1,33 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Popup, PopupTrigger, Type } from 'browndash-components';
-import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx';
+import { ObservableMap, action, computed, makeObservable, observable, observe } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
 import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
+import { DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
-import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents';
+import { DocUtils, Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
-import { undoable, undoBatch } from '../../../util/UndoManager';
+import { undoBatch, undoable } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { EditableView } from '../../EditableView';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { DefaultStyleProvider, StyleProp } from '../../StyleProvider';
 import { Colors } from '../../global/globalEnums';
 import { DocumentView } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { KeyValueBox } from '../../nodes/KeyValueBox';
-import { ObservableReactComponent } from '../../ObservableReactComponent';
-import { DefaultStyleProvider, StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
-import { DocData } from '../../../../fields/DocSymbols';
 const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export enum ColumnType {
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx
index a272ce294..76efe0185 100644
--- a/src/client/views/newlightbox/Header/LightboxHeader.tsx
+++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx
@@ -1,62 +1,70 @@
-import './LightboxHeader.scss';
-import * as React from 'react';
-import { INewLightboxHeader } from "./utils";
-import { NewLightboxView } from '../NewLightboxView';
-import { StrCast } from '../../../../fields/Types';
-import { EditableText } from '../components/EditableText';
-import { getType } from '../utils';
 import { Button, IconButton, Size, Type } from 'browndash-components';
-import { MdExplore, MdTravelExplore } from 'react-icons/md'
-import { BsBookmark, BsBookmarkFill } from 'react-icons/bs'
+import * as React from 'react';
+import { BsBookmark, BsBookmarkFill } from 'react-icons/bs';
+import { MdTravelExplore } from 'react-icons/md';
 import { Doc } from '../../../../fields/Doc';
+import { StrCast } from '../../../../fields/Types';
 import { LightboxView } from '../../LightboxView';
 import { Colors } from '../../global/globalEnums';
-
+import { NewLightboxView } from '../NewLightboxView';
+import { EditableText } from '../components/EditableText';
+import { getType } from '../utils';
+import './LightboxHeader.scss';
+import { INewLightboxHeader } from './utils';
 
 export const NewLightboxHeader = (props: INewLightboxHeader) => {
-    const {height = 100, width} = props;
-    const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc)
-    const [editing, setEditing] = React.useState<boolean>(false)
-    const [title, setTitle] = React.useState<JSX.Element | null>(
-        (null)
-    )
+    const { height = 100, width } = props;
+    const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc);
+    const [editing, setEditing] = React.useState<boolean>(false);
+    const [title, setTitle] = React.useState<JSX.Element | null>(null);
     React.useEffect(() => {
-        let lbDoc = LightboxView.LightboxDoc
-        setDoc(lbDoc)
+        let lbDoc = LightboxView.LightboxDoc;
+        setDoc(lbDoc);
         if (lbDoc) {
             setTitle(
-                <EditableText 
-                editing={editing}
-                text={StrCast(lbDoc.title)} 
-                onEdit={(newText: string) => {
-                    if(lbDoc) lbDoc.title = newText;
-                }} 
-                setEditing={setEditing}            
-            />)
+                <EditableText
+                    editing={editing}
+                    text={StrCast(lbDoc.title)}
+                    onEdit={(newText: string) => {
+                        if (lbDoc) lbDoc.title = newText;
+                    }}
+                    setEditing={setEditing}
+                />
+            );
         }
-    }, [LightboxView.LightboxDoc])
+    }, [LightboxView.LightboxDoc]);
 
-    const [saved, setSaved] = React.useState<boolean>(false)
+    const [saved, setSaved] = React.useState<boolean>(false);
 
-    if (!doc) return null
-    else return <div className={`newLightboxHeader-container`} onPointerDown={(e) => e.stopPropagation()} style={{ minHeight: height, height: height, width: width }}>
-        <div className={`title-container`}>
-            <div className={`lb-label`}>Title</div>
-            {title}
-        </div>
-        <div className={`type-container`}>
-            <div className={`lb-label`}>Type</div>
-            <div className={`type`}>{getType(StrCast(doc.type))}</div>
-        </div>
-        <div style={{gridColumn: 2, gridRow: 1, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
-            <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/>
-            <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/>
-        </div>
-        <div  style={{gridColumn: 2, gridRow: 2, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
-            <Button onClick={() => {
-                console.log(NewLightboxView.ExploreMode)
-                NewLightboxView.SetExploreMode(!NewLightboxView.ExploreMode)
-            }} size={Size.XSMALL} color={Colors.DARK_GRAY} type={Type.SEC} text={"t-SNE 2D Embeddings"} icon={<MdTravelExplore/>}/>
-        </div>
-    </div>
-}
\ No newline at end of file
+    if (!doc) return null;
+    else
+        return (
+            <div className={`newLightboxHeader-container`} onPointerDown={e => e.stopPropagation()} style={{ minHeight: height, height: height, width: width }}>
+                <div className={`title-container`}>
+                    <div className={`lb-label`}>Title</div>
+                    {title}
+                </div>
+                <div className={`type-container`}>
+                    <div className={`lb-label`}>Type</div>
+                    <div className={`type`}>{getType(StrCast(doc.type))}</div>
+                </div>
+                <div style={{ gridColumn: 2, gridRow: 1, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
+                    <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill /> : <BsBookmark />} />
+                    <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill /> : <BsBookmark />} />
+                </div>
+                <div style={{ gridColumn: 2, gridRow: 2, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
+                    <Button
+                        onClick={() => {
+                            console.log(NewLightboxView.ExploreMode);
+                            NewLightboxView.SetExploreMode(!NewLightboxView.ExploreMode);
+                        }}
+                        size={Size.XSMALL}
+                        color={Colors.DARK_GRAY}
+                        type={Type.SEC}
+                        text={'t-SNE 2D Embeddings'}
+                        icon={<MdTravelExplore />}
+                    />
+                </div>
+            </div>
+        );
+};
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
index 9f3c32e4e..1d502b73f 100644
--- a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
@@ -1,74 +1,67 @@
-import { GrClose } from 'react-icons/gr';
-import { IRecommendation, Recommendation } from "../components";
-import './RecommendationList.scss';
+import { IconButton, Size, Type } from 'browndash-components';
 import * as React from 'react';
-import { IRecommendationList } from "./utils";
-import { NewLightboxView } from '../NewLightboxView';
-import { DocCast, StrCast } from '../../../../fields/Types';
 import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
-import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
-import { IDocRequest, fetchKeywords, fetchRecommendations } from '../utils';
-import { IBounds } from '../ExploreView/utils';
+import { GrClose } from 'react-icons/gr';
+import { DocListCast, StrListCast } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
-import { Id } from '../../../../fields/FieldSymbols';
+import { StrCast } from '../../../../fields/Types';
 import { LightboxView } from '../../LightboxView';
-import { IconButton, Size, Type } from 'browndash-components';
 import { Colors } from '../../global/globalEnums';
+import { IBounds } from '../ExploreView/utils';
+import { NewLightboxView } from '../NewLightboxView';
+import { IRecommendation, Recommendation } from '../components';
+import { IDocRequest, fetchKeywords, fetchRecommendations } from '../utils';
+import './RecommendationList.scss';
+import { IRecommendationList } from './utils';
 
 export const RecommendationList = (props: IRecommendationList) => {
-    const {loading, keywords} = props
-    const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true)
-    const [showMore, setShowMore] = React.useState<boolean>(false)
-    const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([])
-    const [update, setUpdate] = React.useState<boolean>(true)
-    const initialRecs: IRecommendation[] = [
-        {loading: true},
-        {loading: true},
-        {loading: true},
-        {loading: true},
-        {loading: true}
-    ];
-    const [recs, setRecs] = React.useState<IRecommendation[]>(initialRecs)
+    const { loading, keywords } = props;
+    const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true);
+    const [showMore, setShowMore] = React.useState<boolean>(false);
+    const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([]);
+    const [update, setUpdate] = React.useState<boolean>(true);
+    const initialRecs: IRecommendation[] = [{ loading: true }, { loading: true }, { loading: true }, { loading: true }, { loading: true }];
+    const [recs, setRecs] = React.useState<IRecommendation[]>(initialRecs);
 
     React.useEffect(() => {
         const getKeywords = async () => {
-            let text = StrCast(LightboxView.LightboxDoc?.text)
-            console.log('[1] fetching keywords')
-            const response = await fetchKeywords(text, 5, true)
-            console.log('[2] response:', response)
+            let text = StrCast(LightboxView.LightboxDoc?.text);
+            console.log('[1] fetching keywords');
+            const response = await fetchKeywords(text, 5, true);
+            console.log('[2] response:', response);
             const kw = response.keywords;
             console.log(kw);
             NewLightboxView.SetKeywords(kw);
             if (LightboxView.LightboxDoc) {
-                console.log('setting keywords on doc')
+                console.log('setting keywords on doc');
                 LightboxView.LightboxDoc.keywords = new List<string>(kw);
                 setKeywordsLoc(NewLightboxView.Keywords);
             }
-            setLoadingKeywords(false)
-        }
-        let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords)
+            setLoadingKeywords(false);
+        };
+        let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords);
         if (!keywordsList || keywordsList.length < 2) {
-            setLoadingKeywords(true)
-            getKeywords()
-            setUpdate(!update)
+            setLoadingKeywords(true);
+            getKeywords();
+            setUpdate(!update);
         } else {
-            setKeywordsLoc(keywordsList)
-            setLoadingKeywords(false)
-            setUpdate(!update)
+            setKeywordsLoc(keywordsList);
+            setLoadingKeywords(false);
+            setUpdate(!update);
         }
-    }, [NewLightboxView.LightboxDoc])
+    }, [NewLightboxView.LightboxDoc]);
 
-    // terms: vannevar bush, information spaces, 
+    // terms: vannevar bush, information spaces,
     React.useEffect(() => {
         const getRecommendations = async () => {
-            console.log('fetching recommendations')
-            let query = 'undefined'
-            if (keywordsLoc) query = keywordsLoc.join(',')
-            let src = StrCast(NewLightboxView.LightboxDoc?.text)
-            let dashDocs:IDocRequest[] = [];
+            console.log('fetching recommendations');
+            let query = 'undefined';
+            if (keywordsLoc) query = keywordsLoc.join(',');
+            let src = StrCast(NewLightboxView.LightboxDoc?.text);
+            let dashDocs: IDocRequest[] = [];
             // get linked docs
-            let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links)
-            console.log("linked docs", linkedDocs)
+            let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links);
+            console.log('linked docs', linkedDocs);
             // get context docs (docs that are also in the collection)
             // let contextDocs: Doc[] = DocListCast(DocCast(LightboxView.LightboxDoc?.context).data)
             // let docId = LightboxView.LightboxDoc && LightboxView.LightboxDoc[Id]
@@ -83,114 +76,142 @@ export const RecommendationList = (props: IRecommendationList) => {
             //         })
             //     }
             // })
-            console.log("dash docs", dashDocs)
+            console.log('dash docs', dashDocs);
             if (query !== undefined) {
-                const response = await fetchRecommendations(src, query, [], true)
-                const num_recs = response.num_recommendations
-                const recs = response.recommendations
-                const keywords = response.keywords
+                const response = await fetchRecommendations(src, query, [], true);
+                const num_recs = response.num_recommendations;
+                const recs = response.recommendations;
+                const keywords = response.keywords;
                 const response_bounds: IBounds = {
-                  max_x: response.max_x,
-                  max_y: response.max_y,
-                  min_x: response.min_x,
-                  min_y: response.min_y
-                }
+                    max_x: response.max_x,
+                    max_y: response.max_y,
+                    min_x: response.min_x,
+                    min_y: response.min_y,
+                };
                 // if (NewLightboxView.NewLightboxDoc) {
                 //     NewLightboxView.NewLightboxDoc.keywords = new List<string>(keywords);
                 //     setKeywordsLoc(NewLightboxView.Keywords);
                 // }
                 // console.log(response_bounds)
-                NewLightboxView.SetBounds(response_bounds)
+                NewLightboxView.SetBounds(response_bounds);
                 const recommendations: IRecommendation[] = [];
                 for (const key in recs) {
-                    console.log(key)
+                    console.log(key);
                     const title = recs[key].title;
-                    const url = recs[key].url
-                    const type = recs[key].type
-                    const text = recs[key].text
-                    const transcript = recs[key].transcript
-                    const previewUrl = recs[key].previewUrl
-                    const embedding = recs[key].embedding
-                    const distance = recs[key].distance
-                    const source = recs[key].source
-                    const related_concepts = recs[key].related_concepts
-                    const docId = recs[key].doc_id
-                    related_concepts.length >= 1 && recommendations.push({
-                        title: title,
-                        data: url,
-                        type: type,
-                        text: text,
-                        transcript: transcript,
-                        previewUrl: previewUrl,
-                        embedding: embedding,
-                        distance: Math.round(distance * 100) / 100,
-                        source: source,
-                        related_concepts: related_concepts,
-                        docId: docId
-                    })
+                    const url = recs[key].url;
+                    const type = recs[key].type;
+                    const text = recs[key].text;
+                    const transcript = recs[key].transcript;
+                    const previewUrl = recs[key].previewUrl;
+                    const embedding = recs[key].embedding;
+                    const distance = recs[key].distance;
+                    const source = recs[key].source;
+                    const related_concepts = recs[key].related_concepts;
+                    const docId = recs[key].doc_id;
+                    related_concepts.length >= 1 &&
+                        recommendations.push({
+                            title: title,
+                            data: url,
+                            type: type,
+                            text: text,
+                            transcript: transcript,
+                            previewUrl: previewUrl,
+                            embedding: embedding,
+                            distance: Math.round(distance * 100) / 100,
+                            source: source,
+                            related_concepts: related_concepts,
+                            docId: docId,
+                        });
                 }
                 recommendations.sort((a, b) => {
                     if (a.distance && b.distance) {
-                        return a.distance - b.distance
-                    } else return 0
-                })
-                console.log("[rec]: ", recommendations)
-                NewLightboxView.SetRecs(recommendations)
-                setRecs(recommendations)
+                        return a.distance - b.distance;
+                    } else return 0;
+                });
+                console.log('[rec]: ', recommendations);
+                NewLightboxView.SetRecs(recommendations);
+                setRecs(recommendations);
             }
-        }
+        };
         getRecommendations();
-    }, [update])
-    
-    
+    }, [update]);
 
-    return <div className={`recommendationlist-container`} onPointerDown={(e) => {e.stopPropagation()}}>
-        <div className={`header`}>
-            <div className={`title`}>
-                Recommendations
-            </div>
-            {NewLightboxView.LightboxDoc && <div style={{fontSize: 10}}>
-                The recommendations are produced based on the text in the document <b><u>{StrCast(NewLightboxView.LightboxDoc.title)}</u></b>. The following keywords are used to fetch the recommendations.
-            </div>}
-            <div className={`lb-label`}>Keywords</div>
-            {loadingKeywords ? <div className={`keywords`}>
-                    <div className={`keyword ${loadingKeywords && 'loading'}`}/>
-                    <div className={`keyword ${loadingKeywords && 'loading'}`}/>
-                    <div className={`keyword ${loadingKeywords && 'loading'}`}/>
-                    <div className={`keyword ${loadingKeywords && 'loading'}`}/>
+    return (
+        <div
+            className={`recommendationlist-container`}
+            onPointerDown={e => {
+                e.stopPropagation();
+            }}>
+            <div className={`header`}>
+                <div className={`title`}>Recommendations</div>
+                {NewLightboxView.LightboxDoc && (
+                    <div style={{ fontSize: 10 }}>
+                        The recommendations are produced based on the text in the document{' '}
+                        <b>
+                            <u>{StrCast(NewLightboxView.LightboxDoc.title)}</u>
+                        </b>
+                        . The following keywords are used to fetch the recommendations.
                     </div>
-                :
-                <div className={`keywords`}>
-                {keywordsLoc && keywordsLoc.map((word, ind) => {
-                    return <div className={`keyword`}>
-                        {word}
-                        <IconButton type={Type.PRIM} size={Size.XSMALL} color={Colors.DARK_GRAY} icon={<GrClose/>} onClick={() => {
-                            let kw = keywordsLoc
-                            kw.splice(ind)
-                            NewLightboxView.SetKeywords(kw)
-                        }}/>
+                )}
+                <div className={`lb-label`}>Keywords</div>
+                {loadingKeywords ? (
+                    <div className={`keywords`}>
+                        <div className={`keyword ${loadingKeywords && 'loading'}`} />
+                        <div className={`keyword ${loadingKeywords && 'loading'}`} />
+                        <div className={`keyword ${loadingKeywords && 'loading'}`} />
+                        <div className={`keyword ${loadingKeywords && 'loading'}`} />
                     </div>
-                })}
-            </div>
-            }
-            {!showMore ?
-            <div className={`lb-caret`} onClick={() => {setShowMore(true)}}>
-                More <FaCaretDown/>
+                ) : (
+                    <div className={`keywords`}>
+                        {keywordsLoc &&
+                            keywordsLoc.map((word, ind) => {
+                                return (
+                                    <div className={`keyword`}>
+                                        {word}
+                                        <IconButton
+                                            type={Type.PRIM}
+                                            size={Size.XSMALL}
+                                            color={Colors.DARK_GRAY}
+                                            icon={<GrClose />}
+                                            onClick={() => {
+                                                let kw = keywordsLoc;
+                                                kw.splice(ind);
+                                                NewLightboxView.SetKeywords(kw);
+                                            }}
+                                        />
+                                    </div>
+                                );
+                            })}
+                    </div>
+                )}
+                {!showMore ? (
+                    <div
+                        className={`lb-caret`}
+                        onClick={() => {
+                            setShowMore(true);
+                        }}>
+                        More <FaCaretDown />
+                    </div>
+                ) : (
+                    <div className={`more`}>
+                        <div
+                            className={`lb-caret`}
+                            onClick={() => {
+                                setShowMore(false);
+                            }}>
+                            Less <FaCaretUp />
+                        </div>
+                        <div className={`lb-label`}>Type</div>
+                        <div className={`lb-label`}>Sources</div>
+                    </div>
+                )}
             </div>
-            :
-            <div className={`more`}>
-                <div className={`lb-caret`} onClick={() => {setShowMore(false)}}>
-                    Less <FaCaretUp/>
-                </div>
-                <div className={`lb-label`}>Type</div>
-                <div className={`lb-label`}>Sources</div>
+            <div className={`recommendations`}>
+                {recs &&
+                    recs.map((rec: IRecommendation) => {
+                        return <Recommendation {...rec} />;
+                    })}
             </div>
-            }
-        </div>
-        <div className={`recommendations`}>
-            {recs && recs.map((rec: IRecommendation) => {
-                return <Recommendation {...rec} />
-            })}
         </div>
-    </div>
-}
\ No newline at end of file
+    );
+};
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 58820a498..a04030a5f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { Howl } from 'howler';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
@@ -50,7 +50,6 @@ import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
-import { InkingStroke } from '../InkingStroke';
 interface Window {
     MediaRecorder: MediaRecorder;
 }
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index c43f89be1..a557cff4f 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -1,6 +1,7 @@
 import { action, makeObservable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { DivHeight, DivWidth } from '../../../Utils';
 import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
@@ -11,7 +12,6 @@ import { LightboxView } from '../LightboxView';
 import './EquationBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
 import EquationEditor from './formattedText/EquationEditor';
-import { DivHeight, DivWidth } from '../../../Utils';
 
 @observer
 export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 7ad250714..36bd037ca 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -15,9 +15,9 @@ import { EditableView } from '../EditableView';
 import { LightboxView } from '../LightboxView';
 import { StyleProp } from '../StyleProvider';
 import { ComparisonBox } from './ComparisonBox';
+import { DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkBox.scss';
-import { DocumentView } from './DocumentView';
 
 @observer
 export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f4d5eef05..434415b96 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { htmlToText } from 'html-to-text';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as WebRequest from 'web-request';
@@ -34,7 +34,7 @@ import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
 import { DocumentView, OpenWhere } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { LinkInfo } from './LinkDocPreview';
 import { PinProps, PresBox } from './trails';
 import './WebBox.scss';
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index ae6da8fb0..cd9fec839 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -18,6 +18,7 @@ import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
+import { dropActionType } from '../../../util/DragManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
 import { SerializationHelper } from '../../../util/SerializationHelper';
@@ -32,11 +33,10 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { dropActionType } from '../../../util/DragManager';
 export interface pinDataTypes {
     scrollable?: boolean;
     dataviz?: number[];
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index addad2bbc..eab33114e 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -3,6 +3,7 @@ import { Button, IconButton, isDark, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { Flip } from 'react-awesome-reveal';
 import { FaBug } from 'react-icons/fa';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { AclAdmin, DashVersion } from '../../../fields/DocSymbols';
@@ -11,6 +12,7 @@ import { GetEffectiveAcl } from '../../../fields/util';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
 import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { DocumentManager } from '../../util/DocumentManager';
+import { dropActionType } from '../../util/DragManager';
 import { PingManager } from '../../util/PingManager';
 import { ReportManager } from '../../util/reportManager/ReportManager';
 import { ServerStats } from '../../util/ServerStats';
@@ -23,11 +25,9 @@ import { CollectionLinearView } from '../collections/collectionLinear';
 import { DashboardView } from '../DashboardView';
 import { Colors } from '../global/globalEnums';
 import { DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView';
+import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './TopBar.scss';
-import { dropActionType } from '../../util/DragManager';
-import { Flip } from 'react-awesome-reveal';
-import { ObservableReactComponent } from '../ObservableReactComponent';
 
 /**
  * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user
-- 
cgit v1.2.3-70-g09d2

